@yoooloo42/bean 1.0.0 → 1.0.2

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/index.js CHANGED
@@ -1,6 +1,8 @@
1
- import dateFormat from './src/dateFormat.js';
1
+ import libs from './src/libs/index.js'
2
+ import utils from './src/utils/index.js';
2
3
 
3
4
  const bean = {
4
- dateFormat
5
+ libs,
6
+ utils
5
7
  }
6
8
  export default bean
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoooloo42/bean",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -16,5 +16,10 @@
16
16
  "bugs": {
17
17
  "url": "https://github.com/13993193075/bean/issues"
18
18
  },
19
- "homepage": "https://github.com/13993193075/bean#readme"
19
+ "homepage": "https://github.com/13993193075/bean#readme",
20
+ "dependencies": {
21
+ "file-saver": "^2.0.5",
22
+ "nodemailer": "^7.0.10",
23
+ "xlsx": "^0.18.5"
24
+ }
20
25
  }
@@ -0,0 +1,128 @@
1
+ import nodemailer from 'nodemailer';
2
+
3
+ /**
4
+ * Node.js 项目中发送电子邮件的函数
5
+ * @param {string} to - 收件人邮箱地址
6
+ * @param {string} subject - 邮件主题
7
+ * @param {string} htmlContent - 邮件的 HTML 内容(正文)
8
+ * @param {string} [textContent] - 邮件的纯文本内容(可选,作为 HTML 无法显示的备用)
9
+ */
10
+ async function sendEmail(to, subject, htmlContent, textContent = '') {
11
+ // ⚠️ 1. 配置 Transporter (SMTP 服务器设置)
12
+ //
13
+ // 如果使用 Gmail:
14
+ // - 您需要启用“两步验证”并生成一个“应用专用密码”(App Password),而不是使用您的主账户密码。
15
+ // - 否则,Google 可能会阻止登录。
16
+ const transporter = nodemailer.createTransport({
17
+ // host: 'smtp.exmail.qq.com', // 替换为你的 SMTP 服务器地址 (例如: 'smtp.gmail.com', 'smtp-mail.outlook.com')
18
+ // port: 465, // 常用端口: 465 (安全连接) 或 587 (TLS)
19
+ // secure: true, // true 为 465 端口, false 为其它端口
20
+ service: '126',
21
+ auth: {
22
+ // user: 'your_email@example.com', // 替换为你的发件箱地址
23
+ user: 'lyxdrwhy000@126.com',
24
+ // pass: 'your_app_password' // 替换为你的邮箱密码或“应用专用密码”
25
+ pass: 'JSQNIEMQCCPCVSJW'
26
+ }
27
+ });
28
+
29
+ // 2. 构造邮件内容对象
30
+ const mailOptions = {
31
+ // from: '"Your Name" <your_email@example.com>', // 发件人信息 (会显示在收件箱中)
32
+ from: `"Stellarium - 企业应用集成平台" <lyxdrwhy000@126.com>`,
33
+ to: to, // 收件人地址
34
+ subject: subject, // 邮件主题
35
+ html: htmlContent, // 邮件 HTML 正文
36
+ text: textContent // 邮件纯文本正文 (可选)
37
+ // 附件可以在这里添加:
38
+ /* attachments: [
39
+ {
40
+ filename: 'report.pdf',
41
+ path: '/path/to/your/report.pdf'
42
+ }
43
+ ] */
44
+ };
45
+
46
+ try {
47
+ // 3. 发送邮件
48
+ let info = await transporter.sendMail(mailOptions);
49
+
50
+ console.log('邮件发送成功:');
51
+ console.log(' 收件人:', to);
52
+ console.log(' 主题:', subject);
53
+ console.log(' Message ID:', info.messageId);
54
+
55
+ return { success: true, messageId: info.messageId };
56
+
57
+ } catch (error) {
58
+ console.error('邮件发送失败:', error);
59
+ return { success: false, error: error.message };
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 生成指定长度的随机数字验证码
65
+ * @param {number} length - 验证码长度 (默认 6 位)
66
+ * @returns {string} - 随机数字字符串
67
+ */
68
+ function vercode(length = 6) {
69
+ let code = '';
70
+ // 确保第一位不是 0,除非长度只有一位
71
+ code += Math.floor(Math.random() * 9) + 1;
72
+
73
+ for (let i = 1; i < length; i++) {
74
+ code += Math.floor(Math.random() * 10);
75
+ }
76
+ return code;
77
+ }
78
+
79
+ /**
80
+ * 发送包含验证码的邮件
81
+ * * @param {string} recipientEmail - 收件人邮箱地址
82
+ * @param {number} [codeLength=6] - 验证码长度
83
+ * @param {number} [expirationMinutes=5] - 验证码有效时间(分钟)
84
+ * @returns {Promise<{success: boolean, code?: string, error?: string}>} - 包含发送结果和生成的验证码
85
+ */
86
+ async function sendEmailVercode(recipientEmail, codeLength = 6, expirationMinutes = 5) {
87
+
88
+ // 1. 生成验证码
89
+ const verificationCode = vercode(codeLength);
90
+
91
+ // 2. 构造邮件内容
92
+ const subject = `您的验证码:${verificationCode},请注意妥善保管,切勿泄露`;
93
+
94
+ const htmlContent = `
95
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: auto; border: 1px solid #ddd; padding: 20px;">
96
+ <h2 style="color: #333;">验证码通知</h2>
97
+ <p>您正在进行Email验证码操作,请在页面中输入以下验证码:</p>
98
+ <div style="background-color: #f5f5f5; padding: 15px; text-align: center; border-radius: 5px; margin: 20px 0;">
99
+ <span style="font-size: 30px; font-weight: bold; color: #007bff;">${verificationCode}</span>
100
+ </div>
101
+ <p style="color: #e44d26;">请注意:该验证码将在 ${expirationMinutes} 分钟内失效。</p>
102
+ <p style="font-size: 12px; color: #999;">如果不是您本人操作,请忽略此邮件。</p>
103
+ </div>
104
+ `;
105
+
106
+ // 3. 调用核心发送函数
107
+ const sendResult = await sendEmail(recipientEmail, subject, htmlContent);
108
+
109
+ // 4. 返回结果和验证码
110
+ if (sendResult.success) {
111
+ return {
112
+ success: true,
113
+ vercode: verificationCode
114
+ };
115
+ } else {
116
+ return {
117
+ success: false,
118
+ error: sendResult.error || '邮件发送失败'
119
+ };
120
+ }
121
+ }
122
+
123
+ const bean = {
124
+ sendEmail,
125
+ vercode,
126
+ sendEmailVercode
127
+ }
128
+ export default bean
@@ -0,0 +1,49 @@
1
+ import * as XLSX from 'xlsx';
2
+ import FileSaver from 'file-saver';
3
+
4
+ /**
5
+ * 将 JSON 数组数据导出为 Excel 文件
6
+ * @param {Array<Object>} json - 要导出的数据数组 (el-table 的 :data 绑定的数据)
7
+ * @param {Array<string>} header - 表格的表头(中文名)
8
+ * @param {Array<string>} keys - 对应表头的数据字段名(英文键名)
9
+ * @param {string} filename - 导出的文件名
10
+ */
11
+ function jsonToExcel({
12
+ json,
13
+ header,
14
+ keys,
15
+ filename = 'excel-file',
16
+ }) {
17
+ // 1. 转换数据格式
18
+ const data = json.map(item => keys.map(key => item[key]));
19
+
20
+ // 2. 将表头和数据组合
21
+ const aoa = [header, ...data];
22
+
23
+ // 3. 创建工作簿和工作表
24
+ const ws = XLSX.utils.aoa_to_sheet(aoa);
25
+ const wb = XLSX.utils.book_new();
26
+ XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
27
+
28
+ // 4. 生成 Excel 文件
29
+ const wbout = XLSX.write(wb, {
30
+ bookType: 'xlsx',
31
+ bookSST: true,
32
+ type: 'array'
33
+ });
34
+
35
+ // 5. 保存文件
36
+ try {
37
+ FileSaver.saveAs(
38
+ new Blob([wbout], { type: 'application/octet-stream' }),
39
+ `${filename}.xlsx`
40
+ );
41
+ } catch (e) {
42
+ if (typeof console !== 'undefined') console.log(e, wbout);
43
+ }
44
+ }
45
+
46
+ const bean = {
47
+ jsonToExcel
48
+ }
49
+ export default bean
@@ -0,0 +1,8 @@
1
+ import Email from "./Email.js"
2
+ import FileSaver from "./FileSaver.js";
3
+
4
+ const bean = {
5
+ Email,
6
+ FileSaver
7
+ }
8
+ export default bean
@@ -0,0 +1,356 @@
1
+ /**
2
+ * 深度拷贝函数
3
+ *
4
+ * @param {any} obj 需要拷贝的对象、数组或基本类型值
5
+ * @param {WeakMap} [cache=new WeakMap()] 用于处理循环引用的缓存
6
+ * @returns {any} 深度拷贝后的新对象/新值
7
+ */
8
+ function deepClone(obj, cache = new WeakMap()) {
9
+ // 1. 基本类型值(包括 null)和函数,直接返回
10
+ if (obj === null || typeof obj !== 'object') {
11
+ return obj;
12
+ }
13
+ // 处理函数(尽管技术上函数是对象,但我们通常不克隆它,而是直接引用)
14
+ if (typeof obj === 'function') {
15
+ return obj;
16
+ }
17
+
18
+ // 2. 检查循环引用
19
+ // 如果缓存中已存在该对象,说明遇到了循环引用,直接返回缓存中的克隆对象
20
+ if (cache.has(obj)) {
21
+ return cache.get(obj);
22
+ }
23
+
24
+ // 3. 处理特定内置对象(Date 和 RegExp)
25
+ if (obj instanceof Date) {
26
+ return new Date(obj.getTime());
27
+ }
28
+ if (obj instanceof RegExp) {
29
+ // g: global, i: ignoreCase, m: multiline, u: unicode, y: sticky
30
+ const flags = obj.global ? 'g' : ''
31
+ + obj.ignoreCase ? 'i' : ''
32
+ + obj.multiline ? 'm' : ''
33
+ + obj.unicode ? 'u' : ''
34
+ + obj.sticky ? 'y' : '';
35
+ return new RegExp(obj.source, flags);
36
+ }
37
+
38
+ // 4. 初始化克隆对象
39
+ // 如果是数组,则初始化为空数组;否则初始化为空对象
40
+ const clone = Array.isArray(obj) ? [] : {};
41
+
42
+ // 将克隆对象放入缓存,以处理接下来的递归调用中可能遇到的循环引用
43
+ cache.set(obj, clone);
44
+
45
+ // 5. 递归拷贝属性
46
+ for (const key in obj) {
47
+ // 确保只处理对象自身的属性,排除原型链上的属性
48
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
49
+ clone[key] = deepClone(obj[key], cache);
50
+ }
51
+ }
52
+
53
+ // 6. 拷贝 Symbol 属性 (ES6/ES2015+)
54
+ if (typeof Object.getOwnPropertySymbols === 'function') {
55
+ Object.getOwnPropertySymbols(obj).forEach(sym => {
56
+ clone[sym] = deepClone(obj[sym], cache);
57
+ });
58
+ }
59
+
60
+ return clone;
61
+ }
62
+
63
+ /**
64
+ * 深度拷贝函数,并在拷贝叶节点值时应用一个转换函数。
65
+ *
66
+ * @param {any} obj - 需要拷贝的对象、数组或基本类型值。
67
+ * @param {function} valueMapper - 接收叶节点值作为参数,并返回新值的转换函数。
68
+ * @param {WeakMap} [cache=new WeakMap()] - 用于处理循环引用的缓存。
69
+ * @returns {any} 深度拷贝并转换后的新对象/新值。
70
+ */
71
+ function deepCloneAndMap(obj, valueMapper, cache = new WeakMap()) {
72
+ // 1. 基本类型值(包括 null)和函数,**应用 valueMapper**
73
+
74
+ // 如果是基本类型值(包括 null),则认为是叶节点,应用 valueMapper
75
+ if (obj === null || typeof obj !== 'object') {
76
+ // 对基本类型值应用转换函数
77
+ return valueMapper(obj);
78
+ }
79
+
80
+ // 如果是函数,通常我们不克隆它,也不转换它的值,直接返回
81
+ if (typeof obj === 'function') {
82
+ return obj;
83
+ }
84
+
85
+ // 2. 检查循环引用 (与原函数逻辑相同)
86
+ if (cache.has(obj)) {
87
+ return cache.get(obj);
88
+ }
89
+
90
+ // 3. 处理特定内置对象(Date 和 RegExp),**应用 valueMapper**
91
+
92
+ // 对于 Date 和 RegExp 这种对象实例,我们通常将它们的**值**视为叶节点,
93
+ // 克隆实例本身后,再对这个克隆实例应用 valueMapper。
94
+ let clone;
95
+
96
+ if (obj instanceof Date) {
97
+ clone = new Date(obj.getTime());
98
+ } else if (obj instanceof RegExp) {
99
+ // 提取 flags
100
+ const flags = (obj.global ? 'g' : '')
101
+ + (obj.ignoreCase ? 'i' : '')
102
+ + (obj.multiline ? 'm' : '')
103
+ + (obj.unicode ? 'u' : '')
104
+ + (obj.sticky ? 'y' : '');
105
+ clone = new RegExp(obj.source, flags);
106
+ }
107
+
108
+ // 检查是否是内置对象(Date/RegExp),如果是,对克隆后的实例应用转换
109
+ if (clone) {
110
+ return valueMapper(clone);
111
+ }
112
+
113
+ // 4. 初始化克隆对象 (普通对象和数组)
114
+ clone = Array.isArray(obj) ? [] : {};
115
+
116
+ // 将克隆对象放入缓存
117
+ cache.set(obj, clone);
118
+
119
+ // 5. 递归拷贝属性
120
+ for (const key in obj) {
121
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
122
+ // 递归调用自身,对子属性进行深拷贝和转换
123
+ clone[key] = deepCloneAndMap(obj[key], valueMapper, cache);
124
+ }
125
+ }
126
+
127
+ // 6. 拷贝 Symbol 属性
128
+ if (typeof Object.getOwnPropertySymbols === 'function') {
129
+ Object.getOwnPropertySymbols(obj).forEach(sym => {
130
+ // 递归调用自身,对 Symbol 属性的值进行深拷贝和转换
131
+ clone[sym] = deepCloneAndMap(obj[sym], valueMapper, cache);
132
+ });
133
+ }
134
+
135
+ // 7. 返回深度克隆并转换后的对象/数组
136
+ return clone;
137
+ }
138
+
139
+ /**
140
+ * 健壮的数据类型判断函数
141
+ *
142
+ * @param {any} value 需要判断类型的值
143
+ * @returns {string} 小写的类型字符串,例如:'string', 'number', 'array', 'function', 'object', 'null', 'undefined'
144
+ */
145
+ function typeOfValue(value) {
146
+ // 1. 处理基本类型值(typeof 准确的部分)
147
+ const type = typeof value;
148
+
149
+ // 'string', 'number', 'boolean', 'symbol', 'bigint', 'function', 'undefined'
150
+ if (type !== 'object') {
151
+ return type;
152
+ }
153
+
154
+ // 2. 处理 null (typeof 的缺陷:typeof null === 'object')
155
+ if (value === null) {
156
+ return 'null';
157
+ }
158
+
159
+ // 3. 处理对象类型值 (使用 Object.prototype.toString.call() 获取内部 [[Class]] 属性)
160
+ // 结果格式为:[object Class]
161
+ const classString = Object.prototype.toString.call(value);
162
+
163
+ // 提取 'Array', 'Date', 'RegExp', 'Map', 'Set', 'Object' 等 Class 名称
164
+ const className = classString.slice(8, -1);
165
+
166
+ // 4. 特殊处理 Array.isArray()
167
+ // 虽然 className 已经是 'Array',但 Array.isArray 更快且明确
168
+ if (className === 'Array') {
169
+ return 'array';
170
+ }
171
+
172
+ // 5. 将其他内置对象和普通对象名称转为小写返回
173
+ return className.toLowerCase();
174
+ }
175
+
176
+ /**
177
+ * 判断一个字符串是否是有效的 JSON 格式
178
+ *
179
+ * @param {string} strValue - 要检查的字符串值
180
+ * @returns {boolean} 如果字符串是有效的 JSON 格式则返回 true,否则返回 false
181
+ */
182
+ function isJsonString(strValue) {
183
+ // 1. 确保输入是一个字符串,如果不是,则直接返回 false
184
+ // JSON 格式只能是字符串
185
+ if (typeof strValue !== 'string') {
186
+ return false;
187
+ }
188
+
189
+ // 2. 尝试解析字符串
190
+ try {
191
+ // 使用 JSON.parse() 尝试解析。
192
+ // 如果解析成功,它就会返回解析后的对象或值。
193
+ // 如果解析失败,就会抛出 SyntaxError 错误。
194
+ JSON.parse(strValue);
195
+
196
+ // 3. 额外的检查 (可选但推荐):
197
+ // 确保解析结果不是原始类型值,例如 "123" 或 "true"
198
+ // 某些场景下,用户希望 JSON 字符串必须是对象或数组的字符串表示
199
+ // const parsed = JSON.parse(strValue);
200
+ // if (typeof parsed !== 'object' || parsed === null) {
201
+ // // 如果你想排除 "123", "null", "true" 这些原始值的 JSON 字符串,可以取消注释
202
+ // // return false;
203
+ // }
204
+
205
+ } catch (e) {
206
+ // 捕获任何解析错误(通常是 SyntaxError),表示格式不符合 JSON 规范
207
+ return false;
208
+ }
209
+
210
+ // 4. 解析成功,返回 true
211
+ return true;
212
+ }
213
+
214
+ /**
215
+ * 遍历树形结构(对象或数组)的所有叶节点,将叶节点的值收集到数组中。
216
+ *
217
+ * 叶节点被定义为:值不是普通对象或数组的节点。
218
+ *
219
+ * @param {object|Array} tree - 要扁平化处理的树形结构对象或数组。
220
+ * @param {Array<any>} [result=[]] - 存储叶节点值的数组(递归内部使用)。
221
+ * @returns {Array<any>} 包含所有叶节点值的数组。
222
+ */
223
+ function flattenTreeValues(tree, result = []) {
224
+ // 确保输入是对象或数组。如果不是,或者为 null,则直接返回结果数组。
225
+ if (typeof tree !== 'object' || tree === null) {
226
+ return result;
227
+ }
228
+
229
+ // 遍历对象的键(如果是数组,键就是索引)
230
+ for (const key in tree) {
231
+ // 确保只处理对象自身的属性,排除原型链上的属性
232
+ if (Object.prototype.hasOwnProperty.call(tree, key)) {
233
+ const value = tree[key];
234
+
235
+ // 判断当前值是否为“叶节点”
236
+ // 检查:值不是对象,且值不是 null
237
+ if (typeof value !== 'object' || value === null) {
238
+ // 1. 如果是基本类型值(叶节点),则将其压入结果数组
239
+ result.push(value);
240
+ } else {
241
+ // 2. 如果是对象或数组(非叶节点),则进行递归调用
242
+ // 注意:这里没有处理 Date, RegExp, Set, Map 等特殊对象,
243
+ // 默认将它们视为非叶节点,直到它们内部的值被遍历完(这通常是不对的)
244
+ //
245
+ // 为了更精确地实现“叶节点”概念,我们将 Date, RegExp 等内置对象视为叶节点:
246
+ const isArray = Array.isArray(value);
247
+ const isPlainObject = !isArray && Object.prototype.toString.call(value) === '[object Object]';
248
+
249
+ if (isArray || isPlainObject) {
250
+ // 如果是普通数组或普通对象,则递归
251
+ flattenTreeValues(value, result);
252
+ } else {
253
+ // 如果是像 Date, RegExp, Map, Set 等特殊内置对象,
254
+ // 我们将其视为叶节点的值(而不是继续遍历其内部结构)
255
+ result.push(value);
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ return result;
262
+ }
263
+
264
+ /**
265
+ * 将扁平化的对象数组转换为树形结构数据,并在每个节点中保留原始数据。
266
+ *
267
+ * @param {Object[]} flatArray 待转化的扁平数组。
268
+ * @param {Object} inputFields 描述输入数组中字段名的对象。
269
+ * @param {string} inputFields.idField 节点代码字段名。
270
+ * @param {string} inputFields.textField 节点文本字段名。
271
+ * @param {string} inputFields.parentField 父节点代码字段名。
272
+ * @param {Object} outputFields 描述输出树形结构中字段名的对象。
273
+ * @param {string} outputFields.idField 输出节点代码字段名 (接收 inputFields.idField 的值)。
274
+ * @param {string} outputFields.textField 输出节点文本字段名 (接收 inputFields.textField 的值)。
275
+ * @param {string} outputFields.childrenField 子节点数组字段名。
276
+ * @param {string} outputFields.originDataField 原始数据字段名 (用于存储原始的 item 对象)。
277
+ * @returns {Object[]} 转换后的树形结构数组。
278
+ */
279
+ function arrayToTree(flatArray, inputFields, outputFields) {
280
+ // 1. 参数校验和字段提取
281
+ if (!Array.isArray(flatArray) || flatArray.length === 0) {
282
+ return [];
283
+ }
284
+
285
+ // 提取输入字段名
286
+ const {
287
+ idField: inputId,
288
+ textField: inputText,
289
+ parentField: inputParent
290
+ } = inputFields;
291
+
292
+ // 提取输出字段名
293
+ const {
294
+ idField: outputId,
295
+ textField: outputText,
296
+ childrenField: outputChildren,
297
+ originDataField: outputRawData // 新增:用于存储原始数据的字段名
298
+ } = outputFields;
299
+
300
+ // 2. 初始化辅助数据结构
301
+ const tree = [];
302
+ const childrenMap = new Map(); // 存储所有节点,键为节点ID
303
+ const rootCandidates = new Set(); // 存储所有可能的根节点ID
304
+
305
+ // 3. 第一次遍历:创建所有节点并建立 ID-Node 映射
306
+ for (const item of flatArray) {
307
+ // 关键:创建一个只包含转换后字段和子节点的结构
308
+ const newNode = {
309
+ // 映射输入字段到输出字段
310
+ [outputId]: item[inputId],
311
+ [outputText]: item[inputText],
312
+ [outputChildren]: [], // 初始化子节点数组
313
+ [outputRawData]: item // 新增:存储原始数据对象
314
+ };
315
+
316
+ childrenMap.set(newNode[outputId], newNode);
317
+ rootCandidates.add(newNode[outputId]); // 假设所有节点都是根节点
318
+ }
319
+
320
+ // 4. 第二次遍历:连接父子关系
321
+ for (const node of childrenMap.values()) {
322
+ const parentId = node[outputRawData][inputParent]; // 从原始数据中获取父ID
323
+
324
+ // 查找父节点
325
+ const parentNode = childrenMap.get(parentId);
326
+
327
+ if (parentNode) {
328
+ // 如果找到了父节点,则将当前节点添加到父节点的子节点列表中
329
+ parentNode[outputChildren].push(node);
330
+
331
+ // 如果当前节点有父节点,它就不是根节点,从根节点候选中移除
332
+ rootCandidates.delete(node[outputId]);
333
+ }
334
+ }
335
+
336
+ // 5. 组装最终树结构
337
+ // 最终的树结构由所有仍在 rootCandidates 列表中的节点组成
338
+ for (const rootId of rootCandidates) {
339
+ const rootNode = childrenMap.get(rootId);
340
+ if (rootNode) {
341
+ tree.push(rootNode);
342
+ }
343
+ }
344
+
345
+ return tree;
346
+ }
347
+
348
+ const bean = {
349
+ deepClone,
350
+ deepCloneAndMap,
351
+ typeOfValue,
352
+ isJsonString,
353
+ flattenTreeValues,
354
+ arrayToTree
355
+ }
356
+ export default bean
@@ -0,0 +1,12 @@
1
+ import dateFormat from './dateFormat.js';
2
+ import deepClone from './deepClone.js';
3
+ import regex from './regex.js'
4
+ import sort from './sort.js';
5
+
6
+ const bean = {
7
+ dateFormat,
8
+ deepClone,
9
+ regex,
10
+ sort
11
+ }
12
+ export default bean
@@ -0,0 +1,119 @@
1
+ /**
2
+ * 校验字符串是否为有效的中国大陆手机号码(宽松校验)
3
+ * 规则:以 1 开头,后面跟 10 个数字,共 11 位。
4
+ *
5
+ * @param {string} str - 需要校验的字符串
6
+ * @returns {boolean} - 如果是有效的手机号格式,返回 true;否则返回 false。
7
+ */
8
+ function cellphone(str) {
9
+ if (typeof str !== 'string' || str.length === 0) {
10
+ return false;
11
+ }
12
+
13
+ // 正则表达式:
14
+ // ^ -> 匹配字符串的开始
15
+ // 1 -> 匹配数字 1
16
+ // \d{10} -> 匹配任何数字 (0-9) 十次
17
+ // $ -> 匹配字符串的结束
18
+ const regex = /^1\d{10}$/;
19
+
20
+ return regex.test(str);
21
+ }
22
+
23
+ /**
24
+ * 校验字符串是否为有效的电子邮件地址格式。
25
+ * * 这个正则是一个常用的、相对宽松但有效的校验模式。
26
+ * 它覆盖了:
27
+ * 1. 局部部分(@符号之前):允许字母、数字、点号(.)、下划线(_)、连字符(-)。
28
+ * 2. 域名部分(@符号之后):至少包含一级域名和顶级域名,允许字母、数字、连字符。
29
+ * 3. 顶级域名(最后一段):必须是至少两位字母(如 .com, .cn, .co.uk)。
30
+ *
31
+ * @param {string} str - 需要校验的字符串
32
+ * @returns {boolean} - 如果是有效的电子邮箱格式,返回 true;否则返回 false。
33
+ */
34
+ function email(str) {
35
+ if (typeof str !== 'string' || str.length === 0) {
36
+ return false;
37
+ }
38
+
39
+ // 正则表达式解释:
40
+ // ^ -> 匹配字符串的开始
41
+ // [a-zA-Z0-9._-]+ -> 局部部分:匹配一个或多个字母、数字、点(.)、下划线(_)或连字符(-)
42
+ // @ -> 必须包含 @ 符号
43
+ // [a-zA-Z0-9.-]+ -> 域名部分:匹配一个或多个字母、数字、点(.)或连字符(-)
44
+ // \. -> 必须包含一个点号(用于分隔域名和顶级域名)
45
+ // [a-zA-Z]{2,6} -> 顶级域名:匹配 2 到 6 个字母 (如 com, cn, info, museum)
46
+ // (?:\.[a-zA-Z]{2,6})? -> (可选) 匹配二级或多级域名(如 .co.uk)
47
+ // $ -> 匹配字符串的结束
48
+
49
+ const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}(?:\.[a-zA-Z]{2,6})?$/;
50
+
51
+ return emailRegex.test(str);
52
+ }
53
+
54
+ /**
55
+ * 校验字符串是否符合中等强度的登录密码要求。
56
+ * 规则:
57
+ * 1. 长度在 8 到 16 位之间。
58
+ * 2. 必须包含大小写字母、数字、特殊符号 (@ $ ! % * # ? &) 中的至少两种组合。
59
+ *
60
+ * @param {string} str - 需要校验的密码字符串
61
+ * @returns {boolean} - 如果符合密码要求,返回 true;否则返回 false。
62
+ */
63
+ function password(str) {
64
+ if (typeof str !== 'string' || str.length === 0) {
65
+ return false;
66
+ }
67
+
68
+ // --- 1. 长度校验 ---
69
+ // 匹配 8 到 16 位任何非换行符的字符
70
+ const lengthRegex = /^.{8,16}$/;
71
+ if (!lengthRegex.test(str)) {
72
+ // console.log("密码长度不符合要求(需 8-16 位)。");
73
+ return false;
74
+ }
75
+
76
+ // --- 2. 字符组合校验(使用正向预查 Lookaheads) ---
77
+
78
+ // 正向预查的定义:
79
+ // (?=.*[a-z]) : 必须包含小写字母
80
+ // (?=.*[A-Z]) : 必须包含大写字母
81
+ // (?=.*\d) : 必须包含数字
82
+ // (?=.*[@$!%*#?&]) : 必须包含特殊符号
83
+
84
+ // 构造四个组合的正则,只要满足任意两个即可
85
+
86
+ let count = 0;
87
+
88
+ // 检查是否包含小写字母
89
+ if (/(?=.*[a-z])/.test(str)) {
90
+ count++;
91
+ }
92
+
93
+ // 检查是否包含大写字母
94
+ if (/(?=.*[A-Z])/.test(str)) {
95
+ count++;
96
+ }
97
+
98
+ // 检查是否包含数字
99
+ if (/(?=.*\d)/.test(str)) {
100
+ count++;
101
+ }
102
+
103
+ // 检查是否包含特殊符号
104
+ if (/(?=.*[@$!%*#?&])/.test(str)) {
105
+ count++;
106
+ }
107
+
108
+ // 最终判断:至少包含两种类型的字符
109
+ const isCombinationValid = count >= 2;
110
+
111
+ // console.log(`满足的字符类型数量: ${count}`);
112
+ return isCombinationValid;
113
+ }
114
+
115
+ const bean = {
116
+ cellphone,
117
+ email,
118
+ password
119
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * 对一个由简单数据类型的值(数值、字符串、日期等)组成的数组进行排序。
3
+ *
4
+ * @param {Array<any>} arr 待排序的简单数据类型数组。
5
+ * @param {'asc' | 'desc'} direction 排序方向:'asc'(升序)或 'desc'(降序)。
6
+ * @param {'locale' | 'natural'} sortType 排序类型:
7
+ * - 'locale': 字典顺序排序。
8
+ * - 'natural': 自然顺序排序。
9
+ * @returns {Array<any>} 排序后的新数组。
10
+ */
11
+ function sortSimpleArray(arr, direction, sortType) {
12
+ // 1. 参数校验 (简化)
13
+ if (!Array.isArray(arr)) {
14
+ console.error("第一个参数必须是一个数组。");
15
+ return [];
16
+ }
17
+ const validDirections = ['asc', 'desc'];
18
+ const validSortTypes = ['locale', 'natural'];
19
+
20
+ if (!validDirections.includes(direction) || !validSortTypes.includes(sortType)) {
21
+ console.error("排序方向或排序类型参数无效。");
22
+ return [...arr];
23
+ }
24
+
25
+ // 2. 创建数组副本
26
+ const sortedArr = arr.slice();
27
+
28
+ // 3. 定义类型处理和排序分组规则
29
+ // JavaScript 的 sort() 方法会将 undefined 和 null 移到末尾,
30
+ // 但为了确保排序稳定性和一致性,我们在这里显式处理它们。
31
+ const comparator = (a, b) => {
32
+ const isNullA = (a === null || typeof a === 'undefined');
33
+ const isNullB = (b === null || typeof b === 'undefined');
34
+
35
+ // 规则 A: 将 null/undefined 分组到末尾(默认行为)
36
+ if (isNullA && !isNullB) return 1;
37
+ if (!isNullA && isNullB) return -1;
38
+ if (isNullA && isNullB) return 0;
39
+
40
+ // 规则 B: 统一转换为字符串进行比较
41
+ // Date 对象会转为 ISO 字符串,Boolean 会转为 'true'/'false'
42
+ const strA = String(a);
43
+ const strB = String(b);
44
+
45
+ let comparison = 0;
46
+
47
+ // --- 内部比较逻辑 ---
48
+ if (sortType === 'locale') {
49
+ // 字典顺序
50
+ if (strA < strB) {
51
+ comparison = -1;
52
+ } else if (strA > strB) {
53
+ comparison = 1;
54
+ }
55
+ } else if (sortType === 'natural') {
56
+ // 自然顺序 (处理数字串和字母数字串)
57
+ comparison = strA.localeCompare(strB, undefined, { numeric: true, sensitivity: 'base' });
58
+ }
59
+
60
+ // 4. 根据排序方向调整结果
61
+ return direction === 'asc' ? comparison : -comparison;
62
+ };
63
+
64
+ // 5. 执行排序
65
+ sortedArr.sort(comparator);
66
+
67
+ return sortedArr;
68
+ }
69
+
70
+ /**
71
+ * 对一个由对象组成的数组进行多级排序。
72
+ *
73
+ * @param {Object[]} arr 待排序的对象数组。
74
+ * @param {Array<{field: string, direction: 'asc'|'desc', sortType: 'locale'|'natural'}>} sortRules 排序规则数组。
75
+ * - field: 用于排序的对象属性名称(字符串)。
76
+ * - direction: 排序方向,'asc'(升序)或 'desc'(降序)。
77
+ * - sortType: 排序类型,'locale'(字典/字母顺序)或 'natural'(自然顺序)。
78
+ * @returns {Object[]} 排序后的新数组。
79
+ */
80
+ function sortObjectArrayMultiLevel(arr, sortRules) {
81
+ // 1. 参数校验
82
+ if (!Array.isArray(arr) || !Array.isArray(sortRules) || sortRules.length === 0) {
83
+ // 如果数组无效或规则为空,则返回数组副本
84
+ return [...arr];
85
+ }
86
+
87
+ // 2. 创建数组副本,以确保函数是纯函数(不修改原始数组)
88
+ const sortedArr = arr.slice();
89
+
90
+ // 3. 定义多级比较器
91
+ const multiLevelComparator = (a, b) => {
92
+ // 遍历所有排序规则,按顺序进行比较
93
+ for (const rule of sortRules) {
94
+ const { field, direction, sortType } = rule;
95
+
96
+ // 检查规则的有效性
97
+ if (!field || !['asc', 'desc'].includes(direction) || !['locale', 'natural'].includes(sortType)) {
98
+ console.warn(`跳过无效的排序规则: ${JSON.stringify(rule)}`);
99
+ continue; // 跳过当前规则,尝试下一个
100
+ }
101
+
102
+ // 获取待比较的字段值
103
+ const valA = a[field];
104
+ const valB = b[field];
105
+
106
+ let comparison = 0;
107
+
108
+ // --- 核心比较逻辑 ---
109
+
110
+ // 为了实现自然排序和字典排序的一致性,我们将值统一转换为字符串进行比较
111
+ const strA = String(valA);
112
+ const strB = String(valB);
113
+
114
+ if (sortType === 'locale') {
115
+ // 字典顺序 (Lexicographical Order)
116
+ if (strA < strB) {
117
+ comparison = -1;
118
+ } else if (strA > strB) {
119
+ comparison = 1;
120
+ }
121
+ } else if (sortType === 'natural') {
122
+ // 自然顺序 (Natural Sort Order)
123
+ // 使用 localeCompare 配合 { numeric: true } 选项
124
+ comparison = strA.localeCompare(strB, undefined, { numeric: true, sensitivity: 'base' });
125
+ }
126
+
127
+ // --- 调整排序结果并返回 ---
128
+
129
+ // 如果 comparison 不为 0,说明在当前字段上找到了差异,可以直接返回结果
130
+ if (comparison !== 0) {
131
+ // 根据排序方向调整比较结果
132
+ return direction === 'asc' ? comparison : -comparison;
133
+ }
134
+
135
+ // 如果 comparison 为 0 (值相等),则继续循环,使用下一个规则进行比较
136
+ }
137
+
138
+ // 如果所有规则都比较完了,值仍然相等,则认为它们的相对顺序不重要
139
+ return 0;
140
+ };
141
+
142
+ // 4. 执行排序
143
+ sortedArr.sort(multiLevelComparator);
144
+
145
+ return sortedArr;
146
+ }
147
+
148
+ /**
149
+ * 对一个由简单数据类型组成的数组进行去重。
150
+ * 使用 Set 对象,这是现代 JavaScript 中最推荐和最简洁的方法。
151
+ *
152
+ * @param {Array<any>} arr 待去重的数组(可包含数值、字符串、布尔值、null、undefined 等)。
153
+ * @returns {Array<any>} 包含唯一元素的新数组。
154
+ */
155
+ const uniqueSimpleArray = (arr) => {
156
+ // 1. 检查输入是否为数组
157
+ if (!Array.isArray(arr)) {
158
+ console.error("输入必须是一个数组。");
159
+ return [];
160
+ }
161
+
162
+ // 2. 创建一个 Set 对象,Set 会自动过滤重复的值
163
+ // 3. 使用扩展运算符 (...) 将 Set 转换回数组
164
+ return [...new Set(arr)];
165
+ }
166
+
167
+ /**
168
+ * 对一个由对象组成的数组进行去重,根据多个指定的字段判断重复。
169
+ * 优化:在判断重复时,同时考虑字段的值和数据类型。
170
+ *
171
+ * @param {Object[]} arr 待去重的对象数组。
172
+ * @param {string[]} fields 用于判断重复的字段名数组。当所有指定字段的值和类型都相同时,视为重复。
173
+ * @returns {Object[]} 包含唯一元素的新数组,保留第一次出现的记录。
174
+ */
175
+ function uniqueObjectArrayByFields(arr, fields) {
176
+ // 1. 参数校验
177
+ if (!Array.isArray(arr) || arr.length === 0) {
178
+ return [];
179
+ }
180
+ if (!Array.isArray(fields) || fields.length === 0) {
181
+ console.warn("未指定用于判断重复的字段,将返回原始数组副本。");
182
+ return [...arr];
183
+ }
184
+
185
+ // 用于存储已见过的唯一键
186
+ const seenKeys = new Set();
187
+ const uniqueArray = [];
188
+ const DELIMITER = "|||";
189
+
190
+ // 2. 遍历原始数组
191
+ for (const item of arr) {
192
+ // 3. 构建唯一键 (同时包含值和类型)
193
+ const keyParts = [];
194
+
195
+ for (const field of fields) {
196
+ const value = item[field];
197
+ const type = typeof value;
198
+
199
+ let valueString;
200
+
201
+ // 确保复杂类型(如对象、日期)也能被正确且唯一地表示
202
+ if (type === 'object' && value !== null) {
203
+ // 对于对象/数组/日期,使用 JSON 序列化
204
+ valueString = JSON.stringify(value);
205
+ } else if (type === 'undefined' || value === null) {
206
+ // 明确区分 undefined 和 null
207
+ valueString = String(value);
208
+ } else {
209
+ // 对于简单类型(string, number, boolean),直接转换为字符串
210
+ valueString = String(value);
211
+ }
212
+
213
+ // 键结构: [值字符串] + [DELIMITER] + [数据类型]
214
+ // 例如: '1|||number' 或 '1|||string'
215
+ keyParts.push(valueString + DELIMITER + type);
216
+ }
217
+
218
+ // 最终键由所有字段的键结构连接而成
219
+ const uniqueKey = keyParts.join(DELIMITER);
220
+
221
+ // 4. 检查是否重复
222
+ if (!seenKeys.has(uniqueKey)) {
223
+ // 如果这个键是第一次出现,则保留
224
+ seenKeys.add(uniqueKey);
225
+ uniqueArray.push(item);
226
+ }
227
+ }
228
+
229
+ return uniqueArray;
230
+ }
231
+
232
+ const bean = {
233
+ sortSimpleArray,
234
+ sortObjectArrayMultiLevel,
235
+ uniqueSimpleArray,
236
+ uniqueObjectArrayByFields
237
+ }
238
+ export default bean
package/test.js ADDED
@@ -0,0 +1 @@
1
+ import bean from './index.js';
File without changes