nhanh-pure-function 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/lib/Index.js +5 -0
- package/lib/User.d.ts +185 -0
- package/lib/User.js +387 -0
- package/lib/Utility.d.ts +152 -0
- package/lib/Utility.js +304 -0
- package/package.json +15 -0
package/lib/Index.js
ADDED
package/lib/User.d.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 添加滚动触底事件
|
|
3
|
+
* @param {滚动标签} element
|
|
4
|
+
* @param {触底事件} callback
|
|
5
|
+
*/
|
|
6
|
+
export function _AddScrollBottomListener(element: HTMLElement, callback: Function): void;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 自动处理 currentPage * pageSize > total
|
|
10
|
+
* @param ask 请求方法
|
|
11
|
+
* @param config 请求参数
|
|
12
|
+
*/
|
|
13
|
+
export function _PagingQuery<Ask extends Function>(
|
|
14
|
+
ask: Ask,
|
|
15
|
+
config: {
|
|
16
|
+
currentPage: number;
|
|
17
|
+
pageSize: number;
|
|
18
|
+
[key: string]: any;
|
|
19
|
+
}
|
|
20
|
+
): ReturnType<Ask> & { currentPage?: number };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 纯数字转 数字加单位
|
|
24
|
+
* @param number 数字或字符串数字
|
|
25
|
+
* @param config : {
|
|
26
|
+
* join 拼接起来吗
|
|
27
|
+
* suffix 后缀
|
|
28
|
+
* integer 不超过万位的数字时保持整数吗
|
|
29
|
+
* }
|
|
30
|
+
* @returns 123456 --> 12.34万 | [ 12.34 , 万 ]
|
|
31
|
+
*/
|
|
32
|
+
export function _FormatNumberWithUnit(
|
|
33
|
+
number: string | number,
|
|
34
|
+
config?: {
|
|
35
|
+
join?: boolean;
|
|
36
|
+
suffix?: string;
|
|
37
|
+
integer?: boolean;
|
|
38
|
+
}
|
|
39
|
+
): string | [number, string];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 设置量词属性
|
|
43
|
+
* @param {*} data 需修改对象
|
|
44
|
+
* @param {*} options 配置
|
|
45
|
+
* @returns data
|
|
46
|
+
*/
|
|
47
|
+
export function _SetQuantifierAttribute<T>(
|
|
48
|
+
data: T,
|
|
49
|
+
options?: (
|
|
50
|
+
| keyof T
|
|
51
|
+
| [
|
|
52
|
+
keyof T,
|
|
53
|
+
{
|
|
54
|
+
join?: boolean;
|
|
55
|
+
suffix?: string;
|
|
56
|
+
integer?: boolean;
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
)[]
|
|
60
|
+
): T;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 为属性值为null | undefined的属性设置默认值
|
|
64
|
+
* @param data 需修改对象
|
|
65
|
+
* @param options 配置
|
|
66
|
+
* @returns
|
|
67
|
+
*/
|
|
68
|
+
export function _SetDefaultValue<T>(
|
|
69
|
+
data: T,
|
|
70
|
+
options: {
|
|
71
|
+
defaultValue?: string;
|
|
72
|
+
fieldsNotRequiringAction?: (string | number | symbol)[];
|
|
73
|
+
} = {}
|
|
74
|
+
): T;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 将字典value转为对应label
|
|
78
|
+
* @param data 需修改对象
|
|
79
|
+
* @param options 配置
|
|
80
|
+
* @returns
|
|
81
|
+
*/
|
|
82
|
+
export function _SetDictionary<T>(
|
|
83
|
+
data: T,
|
|
84
|
+
options: {
|
|
85
|
+
dictionaryOptions?: { [key in keyof T]: { [key: string | number]: any } };
|
|
86
|
+
dictionaryLabel?: (keyof T)[];
|
|
87
|
+
dictionaryLabelJoin?: (keyof T)[];
|
|
88
|
+
defaultValue?: string;
|
|
89
|
+
}
|
|
90
|
+
): T;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 将字符串拼接的图片地址转为数组
|
|
94
|
+
* @param data 需修改对象
|
|
95
|
+
* @param options 配置
|
|
96
|
+
* @returns
|
|
97
|
+
*/
|
|
98
|
+
export function _SetPhoto<T>(
|
|
99
|
+
data: T,
|
|
100
|
+
options: {
|
|
101
|
+
label?: (keyof T)[];
|
|
102
|
+
defaultUrl?: {
|
|
103
|
+
[key in keyof T]: string[];
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
): T;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 将接口返回的数据进行处理,得到展示数据
|
|
110
|
+
* @param data object 类型的数据
|
|
111
|
+
* @param options 配置
|
|
112
|
+
* @returns exhibit_data
|
|
113
|
+
*/
|
|
114
|
+
export function _Exhibit_details<T>(
|
|
115
|
+
data: T,
|
|
116
|
+
options: {
|
|
117
|
+
dictionaryOptions?: { [key in keyof T]: { [key: string | number]: any } };
|
|
118
|
+
dictionaryLabel?: (keyof T)[];
|
|
119
|
+
dictionaryLabelJoin?: (keyof T)[];
|
|
120
|
+
|
|
121
|
+
photoLabel?: (keyof T)[];
|
|
122
|
+
photoDefaultUrl?: {
|
|
123
|
+
[key in keyof T]: string[];
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
quantifierLabel?: (
|
|
127
|
+
| keyof T
|
|
128
|
+
| [
|
|
129
|
+
keyof T,
|
|
130
|
+
{
|
|
131
|
+
join?: boolean;
|
|
132
|
+
suffix?: string;
|
|
133
|
+
integer?: boolean;
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
)[];
|
|
137
|
+
|
|
138
|
+
filterLabel?: (keyof T)[];
|
|
139
|
+
|
|
140
|
+
defaultValue?: string;
|
|
141
|
+
} = {}
|
|
142
|
+
): T;
|
|
143
|
+
|
|
144
|
+
// 定义加载状态更新函数类型
|
|
145
|
+
type LoadingStateUpdater = (newState: boolean) => void;
|
|
146
|
+
// 定义加载控制器的类型
|
|
147
|
+
interface LoadingController {
|
|
148
|
+
invokers: Set<any>; // 假设invoker可以是任何类型
|
|
149
|
+
timer: NodeJS.Timeout | null;
|
|
150
|
+
startTime: number;
|
|
151
|
+
loadingState: LoadingStateUpdater;
|
|
152
|
+
delayTime: number;
|
|
153
|
+
minDisplayTime: number;
|
|
154
|
+
}
|
|
155
|
+
/** 多组loading控制器 */
|
|
156
|
+
export class _LoadingController {
|
|
157
|
+
#controllersCollection: Map<string, LoadingController>;
|
|
158
|
+
|
|
159
|
+
constructor() {}
|
|
160
|
+
|
|
161
|
+
// addController方法的类型定义
|
|
162
|
+
addController(
|
|
163
|
+
key?: string,
|
|
164
|
+
config: {
|
|
165
|
+
loadingState: LoadingStateUpdater;
|
|
166
|
+
delayTime?: number;
|
|
167
|
+
minDisplayTime?: number;
|
|
168
|
+
}
|
|
169
|
+
): void;
|
|
170
|
+
|
|
171
|
+
// deleteController方法的类型定义
|
|
172
|
+
deleteController(key: string): void;
|
|
173
|
+
|
|
174
|
+
// getController方法的类型定义
|
|
175
|
+
getController(key?: string): LoadingController;
|
|
176
|
+
|
|
177
|
+
// resetController方法的类型定义
|
|
178
|
+
resetController(key: string): void;
|
|
179
|
+
|
|
180
|
+
// startLoading方法的类型定义
|
|
181
|
+
startLoading(invoker: any, key?: string): void;
|
|
182
|
+
|
|
183
|
+
// stopLoading方法的类型定义
|
|
184
|
+
stopLoading(invoker: any, key?: string): void;
|
|
185
|
+
}
|
package/lib/User.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { _IsObject, _IsWithinErrorMargin, _NotNull } from './Utility';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 添加滚动触底事件
|
|
5
|
+
* @param {滚动标签} element
|
|
6
|
+
* @param {触底事件} callback
|
|
7
|
+
*/
|
|
8
|
+
export function _AddScrollBottomListener(element, callback) {
|
|
9
|
+
element.addEventListener('scroll', function () {
|
|
10
|
+
if (_IsWithinErrorMargin(element.scrollTop + element.clientHeight, element.scrollHeight, 2)) {
|
|
11
|
+
callback();
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 自动处理 currentPage * pageSize > total
|
|
18
|
+
* @param ask 请求方法
|
|
19
|
+
* @param config 请求参数
|
|
20
|
+
*/
|
|
21
|
+
export function _PagingQuery(ask, config) {
|
|
22
|
+
return new Promise(function (resolve, reject) {
|
|
23
|
+
ask(config)
|
|
24
|
+
.then((data) => {
|
|
25
|
+
const { rows, total } = data;
|
|
26
|
+
if (rows.length == 0 && total > 0) {
|
|
27
|
+
config.currentPage = Math.ceil(total / config.pageSize);
|
|
28
|
+
ask(config)
|
|
29
|
+
.then((data) => {
|
|
30
|
+
data.currentPage = config.currentPage;
|
|
31
|
+
resolve(data);
|
|
32
|
+
})
|
|
33
|
+
.catch(reject);
|
|
34
|
+
} else {
|
|
35
|
+
resolve(data);
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.catch(reject);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 纯数字转 数字加单位
|
|
44
|
+
* @param number 数字或字符串数字
|
|
45
|
+
* @param config : {
|
|
46
|
+
* join 拼接起来吗
|
|
47
|
+
* suffix 后缀
|
|
48
|
+
* integer 不超过万位的数字时保持整数吗
|
|
49
|
+
* }
|
|
50
|
+
* @returns 123456 --> 12.34万 | [ 12.34 , 万 ]
|
|
51
|
+
*/
|
|
52
|
+
export function _FormatNumberWithUnit(number, config = {}) {
|
|
53
|
+
const { join, suffix, integer } = Object.assign(
|
|
54
|
+
{
|
|
55
|
+
join: true,
|
|
56
|
+
suffix: '',
|
|
57
|
+
integer: false
|
|
58
|
+
},
|
|
59
|
+
config
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
function _join(value, suffix, plus = true) {
|
|
63
|
+
value = (plus ? '' : '-') + value;
|
|
64
|
+
if (join) return value + suffix;
|
|
65
|
+
else return [value, suffix];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof number == 'string') {
|
|
69
|
+
if (!/^\d+$/.test(number.trim())) {
|
|
70
|
+
console.error('错误输入:', number);
|
|
71
|
+
return _join(0, suffix);
|
|
72
|
+
}
|
|
73
|
+
} else if (typeof number != 'number') {
|
|
74
|
+
console.error('错误输入:', number);
|
|
75
|
+
return _join(0, suffix);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (Math.abs(number) == Infinity || number == 0) {
|
|
79
|
+
return _join(0, suffix);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
number = Number(number);
|
|
83
|
+
const plus = number >= 0;
|
|
84
|
+
number = Math.abs(number);
|
|
85
|
+
|
|
86
|
+
const units = ['', '万', '亿', '兆', '京', '垓', '秭', '穰', '沟', '涧', '正', '载', '极'];
|
|
87
|
+
const digits = Math.floor(Math.log10(number) / 4); // 计算位数
|
|
88
|
+
|
|
89
|
+
// 不超过万位的数字直接返回
|
|
90
|
+
if (digits === 0) {
|
|
91
|
+
if (integer) {
|
|
92
|
+
return _join(number, suffix, plus);
|
|
93
|
+
} else {
|
|
94
|
+
return _join(number.toFixed(2), suffix, plus);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const dividedNumber = number / Math.pow(10000, digits);
|
|
99
|
+
const formattedNumber = dividedNumber.toFixed(2);
|
|
100
|
+
|
|
101
|
+
return _join(formattedNumber, units[digits] + suffix, plus);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 设置量词属性
|
|
106
|
+
* @param {*} data 需修改对象
|
|
107
|
+
* @param {*} options 配置
|
|
108
|
+
* @returns data
|
|
109
|
+
*/
|
|
110
|
+
export function _SetQuantifierAttribute(data, options = []) {
|
|
111
|
+
if (!_IsObject(data)) {
|
|
112
|
+
console.error('异常输入:', data);
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
options.forEach((item) => {
|
|
117
|
+
if (typeof item === 'string') {
|
|
118
|
+
data[item] = _FormatNumberWithUnit(data[item]);
|
|
119
|
+
} else if (Array.isArray(item)) {
|
|
120
|
+
const [label, config] = data[item];
|
|
121
|
+
if (_NotNull(label) && _IsObject(config)) data[label] = _FormatNumberWithUnit(label, config);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
return data;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 为属性值为null | undefined的属性设置默认值
|
|
129
|
+
* @param data 需修改对象
|
|
130
|
+
* @param options 配置
|
|
131
|
+
* @returns
|
|
132
|
+
*/
|
|
133
|
+
export function _SetDefaultValue(data, options = {}) {
|
|
134
|
+
if (!_IsObject(data)) {
|
|
135
|
+
console.error('异常输入:', data);
|
|
136
|
+
return data;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { defaultValue = '--', fieldsNotRequiringAction } = options;
|
|
140
|
+
|
|
141
|
+
for (const key in data) {
|
|
142
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
143
|
+
const element = data[key];
|
|
144
|
+
if (fieldsNotRequiringAction) {
|
|
145
|
+
if (!fieldsNotRequiringAction.includes(key) && !_NotNull(element)) {
|
|
146
|
+
data[key] = defaultValue;
|
|
147
|
+
}
|
|
148
|
+
} else if (!_NotNull(element)) {
|
|
149
|
+
data[key] = defaultValue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return data;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 将字典value转为对应label
|
|
159
|
+
* @param data 需修改对象
|
|
160
|
+
* @param options 配置
|
|
161
|
+
* @returns
|
|
162
|
+
*/
|
|
163
|
+
export function _SetDictionary(data, options = {}) {
|
|
164
|
+
if (!_IsObject(data)) {
|
|
165
|
+
console.error('异常输入:', data);
|
|
166
|
+
return data;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const {
|
|
170
|
+
dictionaryLabel = [],
|
|
171
|
+
dictionaryLabelJoin = [],
|
|
172
|
+
dictionaryOptions,
|
|
173
|
+
defaultValue = '--'
|
|
174
|
+
} = options;
|
|
175
|
+
|
|
176
|
+
if (dictionaryOptions) {
|
|
177
|
+
dictionaryLabel.forEach((label) => {
|
|
178
|
+
if (_NotNull(data[label])) {
|
|
179
|
+
const options = dictionaryOptions[label];
|
|
180
|
+
|
|
181
|
+
if (options) {
|
|
182
|
+
data[label] = options[data[label]];
|
|
183
|
+
} else {
|
|
184
|
+
data[label] = defaultValue;
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
data[label] = defaultValue;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
dictionaryLabelJoin.forEach((label) => {
|
|
191
|
+
if (_NotNull(data[label]) && data[label] != '') {
|
|
192
|
+
const options = dictionaryOptions[label];
|
|
193
|
+
if (options) {
|
|
194
|
+
const oldvalue = data[label].split(',');
|
|
195
|
+
data[label] = '';
|
|
196
|
+
oldvalue.forEach((_label) => {
|
|
197
|
+
data[label] += options[_label];
|
|
198
|
+
});
|
|
199
|
+
} else {
|
|
200
|
+
data[label] = defaultValue;
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
data[label] = defaultValue;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return data;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 将字符串拼接的图片地址转为数组
|
|
213
|
+
* @param data 需修改对象
|
|
214
|
+
* @param options 配置
|
|
215
|
+
* @returns
|
|
216
|
+
*/
|
|
217
|
+
export function _SetPhoto(data, options = {}) {
|
|
218
|
+
if (!_IsObject(data)) {
|
|
219
|
+
console.error('异常输入:', data);
|
|
220
|
+
return data;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const { label, defaultUrl } = options;
|
|
224
|
+
|
|
225
|
+
if (label) {
|
|
226
|
+
label.forEach((label) => {
|
|
227
|
+
const defaultValue = (defaultUrl && defaultUrl[label]) || [];
|
|
228
|
+
const value = data[label];
|
|
229
|
+
if (typeof value === 'string') {
|
|
230
|
+
data[label] = value.split(',').filter(Boolean);
|
|
231
|
+
} else {
|
|
232
|
+
data[label] = defaultValue;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return data;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 将接口返回的数据进行处理,得到展示数据
|
|
242
|
+
* @param data object 类型的数据
|
|
243
|
+
* @param options 配置
|
|
244
|
+
* @returns exhibit_data
|
|
245
|
+
*/
|
|
246
|
+
export function _Exhibit_details(data, options = {}) {
|
|
247
|
+
if (!_IsObject(data)) {
|
|
248
|
+
console.error('异常输入:', data);
|
|
249
|
+
return {};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
data = JSON.parse(JSON.stringify(data));
|
|
253
|
+
|
|
254
|
+
const {
|
|
255
|
+
dictionaryLabel = [],
|
|
256
|
+
dictionaryLabelJoin = [],
|
|
257
|
+
dictionaryOptions,
|
|
258
|
+
|
|
259
|
+
photoLabel = [],
|
|
260
|
+
photoDefaultUrl,
|
|
261
|
+
|
|
262
|
+
quantifierLabel = [],
|
|
263
|
+
|
|
264
|
+
filterLabel = [],
|
|
265
|
+
|
|
266
|
+
defaultValue = '--'
|
|
267
|
+
} = options;
|
|
268
|
+
|
|
269
|
+
_SetDictionary(data, {
|
|
270
|
+
dictionaryLabel,
|
|
271
|
+
dictionaryLabelJoin,
|
|
272
|
+
dictionaryOptions,
|
|
273
|
+
defaultValue
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
_SetPhoto(data, {
|
|
277
|
+
label: photoLabel,
|
|
278
|
+
defaultUrl: photoDefaultUrl
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
_SetQuantifierAttribute(data, quantifierLabel);
|
|
282
|
+
|
|
283
|
+
_SetDefaultValue(data, {
|
|
284
|
+
defaultValue,
|
|
285
|
+
fieldsNotRequiringAction: dictionaryLabel
|
|
286
|
+
.concat(dictionaryLabelJoin)
|
|
287
|
+
.concat(photoLabel)
|
|
288
|
+
.concat(
|
|
289
|
+
quantifierLabel
|
|
290
|
+
.map((item) => {
|
|
291
|
+
if (typeof item == 'string') return item;
|
|
292
|
+
if (Array.isArray(item)) return item[0];
|
|
293
|
+
})
|
|
294
|
+
.filter(Boolean)
|
|
295
|
+
)
|
|
296
|
+
.concat(filterLabel)
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return data;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** 多组loading控制器 */
|
|
303
|
+
export class _LoadingController {
|
|
304
|
+
#controllersCollection = new Map();
|
|
305
|
+
constructor() {}
|
|
306
|
+
|
|
307
|
+
addController(key = 'default', config) {
|
|
308
|
+
if (this.#controllersCollection.has(key))
|
|
309
|
+
throw new Error('key为: ' + key + ' 的loading控制器已存在, 请重命名。');
|
|
310
|
+
|
|
311
|
+
const {
|
|
312
|
+
loadingState /** 更新/获取 loading 状态的方法 */,
|
|
313
|
+
delayTime = 200 /** 延迟时间 */,
|
|
314
|
+
minDisplayTime = 400 /** 最少显示时间 */
|
|
315
|
+
} = config;
|
|
316
|
+
this.#controllersCollection.set(key, {
|
|
317
|
+
invokers: new Set(),
|
|
318
|
+
timer: null,
|
|
319
|
+
startTime: 0,
|
|
320
|
+
loadingState,
|
|
321
|
+
delayTime,
|
|
322
|
+
minDisplayTime
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
deleteController(key) {
|
|
327
|
+
this.#controllersCollection.delete(key);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getController(key = 'default') {
|
|
331
|
+
const controller = this.#controllersCollection.get(key);
|
|
332
|
+
if (!controller) throw new Error('还未添加key为: ' + key + ' 的loading控制器');
|
|
333
|
+
return controller;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
resetController(key) {
|
|
337
|
+
const controller = this.getController(key);
|
|
338
|
+
const { invokers, loadingState, timer } = controller;
|
|
339
|
+
invokers.clear();
|
|
340
|
+
loadingState(false);
|
|
341
|
+
controller.startTime = 0;
|
|
342
|
+
if (timer) clearTimeout(timer);
|
|
343
|
+
controller.timer = null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
startLoading(invoker, key) {
|
|
347
|
+
const controller = this.getController(key);
|
|
348
|
+
const { invokers, timer, loadingState, delayTime } = controller;
|
|
349
|
+
|
|
350
|
+
invokers.add(invoker);
|
|
351
|
+
|
|
352
|
+
if (invokers.size > 1) return;
|
|
353
|
+
|
|
354
|
+
if (delayTime) {
|
|
355
|
+
if (timer) {
|
|
356
|
+
return;
|
|
357
|
+
} else if (!loadingState()) {
|
|
358
|
+
controller.timer = setTimeout(() => {
|
|
359
|
+
loadingState(true);
|
|
360
|
+
controller.startTime = +new Date();
|
|
361
|
+
controller.timer = null;
|
|
362
|
+
}, delayTime);
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
loadingState(true);
|
|
366
|
+
controller.startTime = +new Date();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
stopLoading(invoker, key) {
|
|
371
|
+
const controller = this.getController(key);
|
|
372
|
+
const { invokers, startTime, minDisplayTime } = controller;
|
|
373
|
+
|
|
374
|
+
const isFinished = invokers.has(invoker) && invokers.size == 1;
|
|
375
|
+
|
|
376
|
+
if (isFinished) {
|
|
377
|
+
const displayTime = +new Date() - startTime;
|
|
378
|
+
if (displayTime >= minDisplayTime) {
|
|
379
|
+
this.resetController(key);
|
|
380
|
+
} else {
|
|
381
|
+
setTimeout(() => this.stopLoading(invoker, key), displayTime - minDisplayTime);
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
invokers.delete(invoker);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
package/lib/Utility.d.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 非null | undefined判断
|
|
3
|
+
* @param value any
|
|
4
|
+
* @returns boolean
|
|
5
|
+
*/
|
|
6
|
+
export function _NotNull(value: any): boolean;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 是正常对象吗
|
|
10
|
+
* @param {} value
|
|
11
|
+
* @returns boolean
|
|
12
|
+
*/
|
|
13
|
+
export function _IsObject(value: any): boolean;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 寻找空闲时机执行传入方法
|
|
17
|
+
* @param callback 需执行的方法
|
|
18
|
+
*/
|
|
19
|
+
export function _ExecuteWhenIdle(callback: Function);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 转为百分比字符串
|
|
23
|
+
* @param value 分子
|
|
24
|
+
* @param totalValue 分母
|
|
25
|
+
* @param decimalPlaces 保留小数位
|
|
26
|
+
* @returns 10.00%
|
|
27
|
+
*/
|
|
28
|
+
export function _ConvertToPercentage(
|
|
29
|
+
value: number,
|
|
30
|
+
totalValue: number,
|
|
31
|
+
decimalPlaces?: number
|
|
32
|
+
): number;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 等待条件满足
|
|
36
|
+
* @param conditionChecker 条件检查器
|
|
37
|
+
* @param timeoutMillis 超时毫秒数
|
|
38
|
+
* @returns Promise<unknown>
|
|
39
|
+
*/
|
|
40
|
+
export function _WaitForCondition(
|
|
41
|
+
conditionChecker: () => boolean,
|
|
42
|
+
timeoutMillis: number
|
|
43
|
+
): Promise;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 排除子串
|
|
47
|
+
* @param inputString 需裁剪字符串
|
|
48
|
+
* @param substringToDelete 被裁减字符串
|
|
49
|
+
* @param delimiter 分隔符
|
|
50
|
+
* @returns 裁减后的字符串
|
|
51
|
+
*/
|
|
52
|
+
export function _ExcludeSubstring(
|
|
53
|
+
inputString: string,
|
|
54
|
+
substringToDelete: string,
|
|
55
|
+
delimiter?: string
|
|
56
|
+
): string;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 首字母大写
|
|
60
|
+
* @param string
|
|
61
|
+
* @returns string
|
|
62
|
+
*/
|
|
63
|
+
export function _CapitalizeFirstLetter(string: string): string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 合并对象 注意: 本函数会直接操作 A
|
|
67
|
+
* @param {Object | Array} A
|
|
68
|
+
* @param {Object | Array} B
|
|
69
|
+
* @returns A&B || B
|
|
70
|
+
*/
|
|
71
|
+
export function _MergeObjects<T, T1>(A: T, B: T1): T & T1;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 时间戳转换字符串
|
|
75
|
+
* @param {Number | Date} time 时间戳或Date对象
|
|
76
|
+
* @param {String} template 完整模板 --> yyyy MM DD hh mm ss ms
|
|
77
|
+
* @param {Boolean} pad 补0
|
|
78
|
+
*/
|
|
79
|
+
export function _TimeTransition(
|
|
80
|
+
time: number | Date,
|
|
81
|
+
template: string,
|
|
82
|
+
pad: boolean
|
|
83
|
+
): string;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 误差范围
|
|
87
|
+
* @param value 需要判断的数字
|
|
88
|
+
* @param target 目标数字
|
|
89
|
+
* @param errorMargin 正负误差范围
|
|
90
|
+
* @returns 是否在误差内
|
|
91
|
+
*/
|
|
92
|
+
export function _IsWithinErrorMargin(
|
|
93
|
+
value: number,
|
|
94
|
+
target: number,
|
|
95
|
+
errorMargin: number
|
|
96
|
+
): boolean;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 读取文件
|
|
100
|
+
* @param src 文件地址
|
|
101
|
+
* @returns 文件的字符串内容
|
|
102
|
+
*/
|
|
103
|
+
export function _ReadFile(src: string): Promise<string>;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 下载文件
|
|
107
|
+
* @param {文件路径} href
|
|
108
|
+
* @param {导出文件名} fileName
|
|
109
|
+
*/
|
|
110
|
+
export function _DownloadFile(href: string, fileName?: string): void;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 获取帧率
|
|
114
|
+
* @param {Function} callback callback( 帧率 , 每帧时间 )
|
|
115
|
+
* @param {Number} referenceNode 参考节点数量
|
|
116
|
+
*/
|
|
117
|
+
export function _GetFrameRate(
|
|
118
|
+
callback: Function,
|
|
119
|
+
referenceNode: number = 10
|
|
120
|
+
): void;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 进度
|
|
124
|
+
* @param {Function} callback callback( 进度百分比 )
|
|
125
|
+
* @param {Number} TIME 总时长
|
|
126
|
+
*/
|
|
127
|
+
export function _Schedule(callback: Function, TIME: number = 500): void;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 格式化数字,给数字加上千位分隔符。
|
|
131
|
+
* @param {number} number - 要格式化的数字。
|
|
132
|
+
* @returns {string} - 格式化后的字符串。
|
|
133
|
+
*/
|
|
134
|
+
export function _FormatNumber(number: number): string;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 单位转换 12** -> **px
|
|
138
|
+
* @param {string} width
|
|
139
|
+
* @returns 对应的单位为px的宽
|
|
140
|
+
*/
|
|
141
|
+
export function _GetOtherSizeInPixels(width: string): string;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 驼峰命名
|
|
145
|
+
* @param {字符串} str
|
|
146
|
+
* @param {是否删除分割字符} isRemoveDelimiter
|
|
147
|
+
* @returns 'wq1wqw-qw2qw' -> 'wq1Wqw-Qw2Qw' / 'wqWqwQwQw'
|
|
148
|
+
*/
|
|
149
|
+
export function _ConvertToCamelCase(
|
|
150
|
+
str: string,
|
|
151
|
+
isRemoveDelimiter?: boolean
|
|
152
|
+
): string;
|
package/lib/Utility.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 非null | undefined判断
|
|
3
|
+
* @param value any
|
|
4
|
+
* @returns boolean
|
|
5
|
+
*/
|
|
6
|
+
export function _NotNull(value) {
|
|
7
|
+
return value !== null && value !== undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 是正常对象吗
|
|
12
|
+
* @param {} value
|
|
13
|
+
* @returns boolean
|
|
14
|
+
*/
|
|
15
|
+
export function _IsObject(value) {
|
|
16
|
+
return !(value === null || typeof value !== "object" || Array.isArray(value));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 寻找空闲时机执行传入方法
|
|
21
|
+
* @param callback 需执行的方法
|
|
22
|
+
*/
|
|
23
|
+
export function _ExecuteWhenIdle(callback) {
|
|
24
|
+
if (typeof callback !== "function")
|
|
25
|
+
return console.error("非函数:", callback);
|
|
26
|
+
const loop = function (deadline) {
|
|
27
|
+
if (deadline.didTimeout || deadline.timeRemaining() <= 0)
|
|
28
|
+
requestIdleCallback(loop);
|
|
29
|
+
else callback();
|
|
30
|
+
};
|
|
31
|
+
requestIdleCallback(loop);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 转为百分比字符串
|
|
36
|
+
* @param value 分子
|
|
37
|
+
* @param totalValue 分母
|
|
38
|
+
* @param decimalPlaces 保留小数位
|
|
39
|
+
* @returns 10.00%
|
|
40
|
+
*/
|
|
41
|
+
export function _ConvertToPercentage(value, totalValue, decimalPlaces = 2) {
|
|
42
|
+
if (
|
|
43
|
+
typeof value !== "number" ||
|
|
44
|
+
typeof totalValue !== "number" ||
|
|
45
|
+
typeof decimalPlaces !== "number" ||
|
|
46
|
+
totalValue == 0
|
|
47
|
+
) {
|
|
48
|
+
console.error("异常输入:", arguments);
|
|
49
|
+
return "0.00%";
|
|
50
|
+
}
|
|
51
|
+
return (
|
|
52
|
+
Number(
|
|
53
|
+
parseInt((value / totalValue) * Math.pow(10, 2 + decimalPlaces)) /
|
|
54
|
+
Math.pow(10, decimalPlaces)
|
|
55
|
+
) || 0
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 等待条件满足
|
|
61
|
+
* @param conditionChecker 条件检查器
|
|
62
|
+
* @param timeoutMillis 超时毫秒数
|
|
63
|
+
* @returns Promise<unknown>
|
|
64
|
+
*/
|
|
65
|
+
export function _WaitForCondition(conditionChecker, timeoutMillis) {
|
|
66
|
+
const startTime = new Date() - 0;
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const checkCondition = () => {
|
|
69
|
+
const nowTime = new Date() - 0;
|
|
70
|
+
if (nowTime - startTime >= timeoutMillis) return reject("超时");
|
|
71
|
+
if (conditionChecker()) return resolve("完成");
|
|
72
|
+
requestIdleCallback(checkCondition);
|
|
73
|
+
};
|
|
74
|
+
checkCondition();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 排除子串
|
|
80
|
+
* @param inputString 需裁剪字符串
|
|
81
|
+
* @param substringToDelete 被裁减字符串
|
|
82
|
+
* @param delimiter 分隔符
|
|
83
|
+
* @returns 裁减后的字符串
|
|
84
|
+
*/
|
|
85
|
+
export function _ExcludeSubstring(
|
|
86
|
+
inputString,
|
|
87
|
+
substringToDelete,
|
|
88
|
+
delimiter = ","
|
|
89
|
+
) {
|
|
90
|
+
const regex = new RegExp(
|
|
91
|
+
`(^|${delimiter})${substringToDelete}(${delimiter}|$)`,
|
|
92
|
+
"g"
|
|
93
|
+
);
|
|
94
|
+
return inputString.replace(regex, function ($0, $1, $2) {
|
|
95
|
+
return $1 === $2 ? delimiter : "";
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 首字母大写
|
|
101
|
+
* @param str
|
|
102
|
+
* @returns string
|
|
103
|
+
*/
|
|
104
|
+
export function _CapitalizeFirstLetter(string) {
|
|
105
|
+
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 合并对象 注意: 本函数会直接操作 A
|
|
110
|
+
* @param {Object | Array} A
|
|
111
|
+
* @param {Object | Array} B
|
|
112
|
+
* @returns A&B || B
|
|
113
|
+
*/
|
|
114
|
+
export function _MergeObjects(A, B, visitedObjects = []) {
|
|
115
|
+
const getType = (v) => (Array.isArray(v) ? "array" : typeof v);
|
|
116
|
+
const TA = getType(A);
|
|
117
|
+
const TB = getType(B);
|
|
118
|
+
|
|
119
|
+
if (TA != TB) return B;
|
|
120
|
+
if (visitedObjects.some((item) => item == B)) return B;
|
|
121
|
+
|
|
122
|
+
if (TA == "object") {
|
|
123
|
+
visitedObjects.push(A, B);
|
|
124
|
+
for (const key in B) {
|
|
125
|
+
if (Object.prototype.hasOwnProperty.call(B, key)) {
|
|
126
|
+
const BC = B[key];
|
|
127
|
+
const AC = A[key];
|
|
128
|
+
const fianlValue = _MergeObjects(AC, BC, visitedObjects);
|
|
129
|
+
A[key] = fianlValue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return A;
|
|
133
|
+
} else if (TA == "array") {
|
|
134
|
+
visitedObjects.push(A, B);
|
|
135
|
+
B.forEach((item, index) => {
|
|
136
|
+
const BC = item;
|
|
137
|
+
const AC = A[index];
|
|
138
|
+
const fianlValue = _MergeObjects(AC, BC, visitedObjects);
|
|
139
|
+
A[index] = fianlValue;
|
|
140
|
+
});
|
|
141
|
+
return A;
|
|
142
|
+
} else return B;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 时间戳转换字符串
|
|
147
|
+
* @param {Number | Date} time 时间戳或Date对象
|
|
148
|
+
* @param {String} template 完整模板 --> yyyy MM DD hh mm ss ms
|
|
149
|
+
* @param {Boolean} pad 补0
|
|
150
|
+
*/
|
|
151
|
+
export function _TimeTransition(time, template, pad = true) {
|
|
152
|
+
try {
|
|
153
|
+
time = new Date(time);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(error);
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
const dictionary = {
|
|
159
|
+
yyyy: "getFullYear",
|
|
160
|
+
MM: "getMonth",
|
|
161
|
+
DD: "getDate",
|
|
162
|
+
hh: "getHours",
|
|
163
|
+
mm: "getMinutes",
|
|
164
|
+
ss: "getSeconds",
|
|
165
|
+
ms: (num) => +num % 1000,
|
|
166
|
+
};
|
|
167
|
+
for (const key in dictionary) {
|
|
168
|
+
if (Object.hasOwnProperty.call(dictionary, key)) {
|
|
169
|
+
if (new RegExp(key).test(template)) {
|
|
170
|
+
let value,
|
|
171
|
+
fun = dictionary[key];
|
|
172
|
+
|
|
173
|
+
if (typeof fun == "function") value = fun(time);
|
|
174
|
+
else value = time[fun]();
|
|
175
|
+
|
|
176
|
+
if (key == "MM") value++;
|
|
177
|
+
|
|
178
|
+
if (pad) value = String(value).padStart(2, "0");
|
|
179
|
+
|
|
180
|
+
template = template.replace(key, value);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return template;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 误差范围
|
|
189
|
+
* @param value 需要判断的数字
|
|
190
|
+
* @param target 目标数字
|
|
191
|
+
* @param errorMargin 正负误差范围
|
|
192
|
+
* @returns 是否在误差内
|
|
193
|
+
*/
|
|
194
|
+
export function _IsWithinErrorMargin(value, target, errorMargin) {
|
|
195
|
+
return Math.abs(value - target) <= errorMargin;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 读取文件
|
|
200
|
+
* @param src 文件地址
|
|
201
|
+
* @returns 文件的字符串内容
|
|
202
|
+
*/
|
|
203
|
+
export function _ReadFile(src) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
fetch(src)
|
|
206
|
+
.then((response) => resolve(response.text()))
|
|
207
|
+
.catch((error) => {
|
|
208
|
+
console.error("Error fetching :", error);
|
|
209
|
+
reject(error);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 下载文件
|
|
216
|
+
* @param {文件路径} href
|
|
217
|
+
* @param {导出文件名} download
|
|
218
|
+
*/
|
|
219
|
+
export function _DownloadFile(href, fileName) {
|
|
220
|
+
const a = document.createElement("a");
|
|
221
|
+
a.href = href;
|
|
222
|
+
if (fileName) a.download = fileName;
|
|
223
|
+
a.style.display = "none";
|
|
224
|
+
document.body.appendChild(a);
|
|
225
|
+
a.click();
|
|
226
|
+
a.remove();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 获取帧率
|
|
231
|
+
* @param {Function} callback callback( 帧率 , 每帧时间 )
|
|
232
|
+
* @param {Number} referenceNode 参考节点数量
|
|
233
|
+
*/
|
|
234
|
+
export function _GetFrameRate(callback, referenceNode = 10) {
|
|
235
|
+
let t,
|
|
236
|
+
arr = [];
|
|
237
|
+
function loop(time) {
|
|
238
|
+
if (t) {
|
|
239
|
+
arr.push(time - t);
|
|
240
|
+
let l = arr.length;
|
|
241
|
+
if (l >= referenceNode) {
|
|
242
|
+
let num = arr.reduce((a, b) => a + b, 0);
|
|
243
|
+
num /= l;
|
|
244
|
+
callback(1000 / num, num);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
t = time;
|
|
249
|
+
requestAnimationFrame(loop);
|
|
250
|
+
}
|
|
251
|
+
requestAnimationFrame(loop);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 进度
|
|
256
|
+
* @param {Function} callback callback( 进度百分比 )
|
|
257
|
+
* @param {Number} TIME 总时长
|
|
258
|
+
*/
|
|
259
|
+
export function _Schedule(callback, TIME = 500) {
|
|
260
|
+
let t;
|
|
261
|
+
function loop(time) {
|
|
262
|
+
if (!t) t = time;
|
|
263
|
+
let percentage = Math.min((time - t) / TIME, 1);
|
|
264
|
+
callback(percentage);
|
|
265
|
+
if (time - t < TIME) requestAnimationFrame(loop);
|
|
266
|
+
}
|
|
267
|
+
requestAnimationFrame(loop);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 格式化数字,给数字加上千位分隔符。
|
|
272
|
+
* @param {number} number - 要格式化的数字。
|
|
273
|
+
* @returns {string} - 格式化后的字符串。
|
|
274
|
+
*/
|
|
275
|
+
export function _FormatNumber(number) {
|
|
276
|
+
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 单位转换 12** -> **px
|
|
281
|
+
* @param {string} width
|
|
282
|
+
* @returns 对应的单位为px的宽
|
|
283
|
+
*/
|
|
284
|
+
export function _GetOtherSizeInPixels(width) {
|
|
285
|
+
if (/px/.test(width)) return width;
|
|
286
|
+
const dom = document.createElement("div");
|
|
287
|
+
dom.style.width = width;
|
|
288
|
+
document.body.appendChild(dom);
|
|
289
|
+
width = parseFloat(window.getComputedStyle(dom).width);
|
|
290
|
+
document.body.removeChild(dom);
|
|
291
|
+
return width;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 驼峰命名
|
|
296
|
+
* @param {字符串} str
|
|
297
|
+
* @param {是否删除分割字符} isRemoveDelimiter
|
|
298
|
+
* @returns 'wq1wqw-qw2qw' -> 'wq1Wqw-Qw2Qw' / 'wqWqwQwQw'
|
|
299
|
+
*/
|
|
300
|
+
export function _ConvertToCamelCase(str, isRemoveDelimiter) {
|
|
301
|
+
str = str.replace(/([^a-zA-Z][a-z])/g, (match) => match.toUpperCase());
|
|
302
|
+
if (isRemoveDelimiter) return str.replace(/[^a-zA-Z]+/g, "");
|
|
303
|
+
return str;
|
|
304
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nhanh-pure-function",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "纯函数工具",
|
|
5
|
+
"main": "lib/Index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"pure-function",
|
|
11
|
+
"format"
|
|
12
|
+
],
|
|
13
|
+
"author": "nhanh",
|
|
14
|
+
"license": "MIT"
|
|
15
|
+
}
|