【同步】前端项目源码

【修复】工作流兼容问题
This commit is contained in:
chudong
2025-05-10 11:53:11 +08:00
parent c514471adc
commit f1a75afaba
584 changed files with 55714 additions and 110 deletions

BIN
frontend/packages/utils/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,148 @@
# 浏览器工具函数文档
这个模块提供了一系列用于浏览器端操作的实用工具函数。
## 目录
1. [浏览器环境检测](#浏览器环境检测)
2. [浏览器信息获取](#浏览器信息获取)
3. [缓存操作](#缓存操作)
4. [Cookie 操作](#cookie-操作)
5. [Storage 操作](#storage-操作)
## 浏览器环境检测
### isHttps
检查当前页面是否使用 HTTPS 协议。
```typescript
const isSecure = isHttps() // 返回 boolean
```
### isDev
判断当前是否为开发环境。
```typescript
const isDevelopment = isDev() // 返回 boolean
```
## 浏览器信息获取
### getBrowserOSInfo
获取当前浏览器和操作系统信息。
```typescript
const { browser, os } = getBrowserOSInfo()
// 返回格式:{ browser: string, os: string }
```
### getScreenInfo
获取屏幕分辨率和设备像素比信息。
```typescript
const { resolution, scale } = getScreenInfo()
// 返回格式:{ resolution: string, scale: number }
```
## 缓存操作
### forceRefresh
强制刷新页面并清理所有缓存(包括 Cache API、localStorage 和 sessionStorage
```typescript
await forceRefresh()
```
### clearBrowserCache
清空浏览器所有缓存数据。
```typescript
clearBrowserCache()
```
## Cookie 操作
### setCookie
设置 Cookie 值。
```typescript
setCookie(key: string, value: string, days?: number)
```
### getCookie
获取 Cookie 值。
```typescript
const value = getCookie(key: string) // 返回 string | null
```
### deleteCookie
删除指定的 Cookie。
```typescript
deleteCookie(key: string)
```
### clearCookie
清空所有 Cookie。
```typescript
clearCookie()
```
## Storage 操作
### LocalStorage 操作
```typescript
// 设置数据
setLocalItem(key: string, value: any)
// 获取数据
const value = getLocalItem(key: string)
// 删除数据
removeLocalItem(key: string)
// 清空所有数据
clearLocal()
```
### SessionStorage 操作
```typescript
// 设置数据
setSessionItem(key: string, value: any)
// 获取数据
const value = getSessionItem(key: string)
// 删除数据
removeSessionItem(key: string)
// 清空所有数据
clearSession()
```
## 特点
1. 使用 TypeScript 编写,提供完整的类型支持
2. 使用 Ramda.js 进行函数式编程
3. 支持数据自动序列化和反序列化
4. 提供柯里化版本的函数供函数式编程使用
## 注意事项
1. Cookie 操作会自动根据 HTTPS 协议添加前缀
2. Storage 操作会自动进行 JSON 序列化和反序列化
3. 所有清除缓存的操作都是不可逆的,请谨慎使用

View File

@@ -0,0 +1,219 @@
# 业务工具函数文档
这个模块提供了一系列用于业务处理的实用工具函数。该模块使用 Ramda.js 进行函数式编程,并提供了完整的 TypeScript 类型支持。
## 目录
1. [正则验证](#正则验证)
2. [业务操作](#业务操作)
3. [代理函数](#代理函数)
## 正则验证
### 邮箱验证
```typescript
isEmail(email: string): boolean
```
验证字符串是否为有效的邮箱地址。使用标准邮箱格式验证,要求包含 `@` 和域名。
### 手机号验证
```typescript
isPhone(phone: string): boolean
```
验证字符串是否为有效的中国大陆手机号。要求以 1 开头,第二位为 3-9总长度为 11 位。
### 身份证号验证
```typescript
isIdCard(idCard: string): boolean
```
验证字符串是否为有效的中国大陆身份证号。支持 18 位身份证号码验证,包含生日和校验位检查。
### URL验证
```typescript
isUrl(url: string): boolean
```
验证字符串是否为有效的URL。支持 http、https、ftp、rtsp、mms 等协议。
### IP地址验证
#### IPv4验证
```typescript
isIpv4(ip: string): boolean
```
验证字符串是否为有效的IPv4地址。每段数字范围为 0-255使用更精确的数字范围验证。
#### IPv6验证
```typescript
isIpv6(ip: string): boolean
```
验证字符串是否为有效的IPv6地址。支持以下格式
- 标准 IPv6 地址
- 压缩形式
- 混合形式
- IPv4 映射到 IPv6
- 特殊形式(如 fe80:: 链路本地地址)
#### 通用IP验证
```typescript
isIp(ip: string): boolean
```
验证字符串是否为有效的IP地址同时支持 IPv4 和 IPv6。
### IP段验证
```typescript
isIps(ips: string): boolean
```
验证字符串是否为有效的IP段。支持 CIDR 表示法,如 "192.168.1.0/24"。
### 端口验证
```typescript
isPort(port: string): boolean
```
验证字符串是否为有效的端口号。范围为 1-65535使用精确的数字范围验证。
### MAC地址验证
```typescript
isMac(mac: string): boolean
```
验证字符串是否为有效的MAC地址。格式为 XX-XX-XX-XX-XX-XX其中 X 为十六进制数字。
### 中文验证
```typescript
isChinese(str: string): boolean
```
验证字符串是否只包含中文字符。使用 Unicode 范围 \u4e00-\u9fa5 进行验证。
## 业务操作
### 手机号加密
```typescript
encryptPhone(phone: string): string
```
将手机号中间4位替换为星号。
示例:
```typescript
encryptPhone('13812345678') // 返回: '138****5678'
```
### 身份证号加密
```typescript
encryptIdCard(idCard: string): string
```
将身份证号中间4位替换为星号。
示例:
```typescript
encryptIdCard('440101199001011234') // 返回: '440101****1234'
```
### 版本号比较
```typescript
compareVersion(version1: string, version2: string): number
```
使用函数式编程方式比较两个版本号的大小。
- 返回 1: version1 > version2
- 返回 -1: version1 < version2
- 返回 0: version1 = version2
特点:
- 使用 Ramda.js 的 pipe 函数进行函数组合
- 自动处理不同长度的版本号
- 支持任意深度的版本号比较
### 字节转换
```typescript
formatBytes(bytes: number, fixed?: number, isUnit?: boolean, endUnit?: string): string
formatBytesCurried(bytes: number)(fixed?: number, isUnit?: boolean, endUnit?: string): string
```
将字节数转换为可读的字符串,提供普通版本和柯里化版本。
参数:
- `bytes`: 要转换的字节数
- `fixed`: 保留小数位数,默认为 2
- `isUnit`: 是否显示单位,默认为 true
- `endUnit`: 指定结束单位,如果指定则转换到该单位为止
支持的单位B、KB、MB、GB、TB
### 分页字符串转换
```typescript
formatPage(page: string): number
```
从特定格式的字符串中提取分页数量。使用正则表达式匹配 `class='Pcount'>共n条<` 格式的字符串。
## 代理函数
### 代理配置生成
```typescript
getProxyConfig(proxyKey: string, usage?: 'query' | 'params'): string | { request_time: number; request_token: string }
```
生成代理请求所需的配置信息。使用 MD5 加密生成请求令牌。
参数:
- `proxyKey`: 代理密钥
- `usage`: 使用场景
- 'params': 返回对象格式
- 'query': 返回查询字符串格式
返回值:
- params 格式: `{ request_time: number; request_token: string }`
- query 格式: `request_time=${time}&request_token=${token}`
## 特点
1. 使用 TypeScript 编写,提供完整的类型支持
2. 使用 Ramda.js 进行函数式编程
3. 提供柯里化版本的函数
4. 使用精确的正则表达式进行验证
5. 支持现代化的 IP 地址格式(包括 IPv6
## 注意事项
1. 所有正则验证函数都使用严格的匹配规则
2. 版本号比较支持任意深度的版本号格式
3. 字节转换函数支持自定义结束单位
4. 代理配置函数仅在开发环境下使用
5. 所有函数都经过优化,支持函数式编程范式

View File

@@ -0,0 +1,274 @@
# 数据处理工具函数文档
这个模块提供了一系列用于数据处理的实用工具函数。该模块使用 Ramda.js 进行函数式编程,并提供了完整的 TypeScript 类型支持。
## 目录
1. [数据转换](#数据转换)
2. [数据校验](#数据校验)
3. [数据过滤与重组](#数据过滤与重组)
4. [数据映射](#数据映射)
## 数据转换
### 对象值转字符串
```typescript
objectToString(obj: Record<string, any>): Record<string, string>
```
将对象的所有值转换为字符串。使用 Ramda.js 的 `map` 函数进行转换。
示例:
```typescript
objectToString({ age: 25, score: 98.5 }) // 返回: { age: "25", score: "98.5" }
```
### 数组转对象
```typescript
arrayToObject<T extends Record<string, any>>(key: string, array: T[]): Record<string, T>
```
将数组转换为对象,使用指定的 key 作为新对象的键。提供柯里化支持。
示例:
```typescript
const users = [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
]
arrayToObject('id', users) // 返回: { '1': { id: '1', name: 'Alice' }, '2': { id: '2', name: 'Bob' } }
```
### 对象深度扁平化
```typescript
flattenObject<T extends Record<string, any>>(obj: T): Record<string, any>
```
将嵌套的对象结构扁平化,使用点号连接键名。
示例:
```typescript
const nested = {
user: {
info: {
name: 'Alice',
age: 25,
},
},
}
flattenObject(nested) // 返回: { 'user.info.name': 'Alice', 'user.info.age': 25 }
```
## 数据校验
### 正则匹配验证
```typescript
matchesPattern<T extends RegExp>(pattern: T, str: string): boolean
```
验证字符串是否符合指定的正则表达式模式。提供柯里化支持。
示例:
```typescript
const isEmail = matchesPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
isEmail('test@example.com') // 返回: true
```
### 必需键验证
```typescript
hasRequiredKeys<T extends Record<string, any>>(requiredKeys: string[], obj: T): boolean
```
验证对象是否包含所有指定的必需键。提供柯里化支持。
示例:
```typescript
const requiredFields = ['name', 'email']
hasRequiredKeys(requiredFields, { name: 'Alice', email: 'alice@example.com' }) // 返回: true
```
### 数值范围验证
```typescript
isInRange<T extends number>(min: T, max: T, value: T): boolean
```
验证数值是否在指定的范围内。提供柯里化支持。
示例:
```typescript
const isValidAge = isInRange(0, 120)
isValidAge(25) // 返回: true
```
## 数据过滤与重组
### 对象属性过滤
```typescript
filterObject<T extends Record<string, any>>(predicate: (value: any) => boolean, obj: T): Record<string, any>
```
根据条件函数过滤对象的属性。提供柯里化支持。
示例:
```typescript
const removeEmpty = filterObject((value) => value !== '')
removeEmpty({ name: 'Alice', title: '', age: 25 }) // 返回: { name: 'Alice', age: 25 }
```
### 数组分组
```typescript
groupByKey<T extends Record<string, any>>(key: string, array: T[]): Record<string, T[]>
```
按照指定的键对数组进行分组。提供柯里化支持。
示例:
```typescript
const users = [
{ role: 'admin', name: 'Alice' },
{ role: 'user', name: 'Bob' },
{ role: 'admin', name: 'Charlie' },
]
groupByKey('role', users)
// 返回: {
// admin: [{ role: 'admin', name: 'Alice' }, { role: 'admin', name: 'Charlie' }],
// user: [{ role: 'user', name: 'Bob' }]
// }
```
### 深层属性提取
```typescript
pluckDeep<T extends Record<string, any>>(path: string[], list: T[]): T[]
```
从对象数组中提取指定路径的值。提供柯里化支持。
示例:
```typescript
const users = [{ info: { name: 'Alice', age: 25 } }, { info: { name: 'Bob', age: 30 } }]
pluckDeep(['info', 'name'], users) // 返回: ['Alice', 'Bob']
```
### 数组扁平化去重
```typescript
flattenAndUniq<T>(array: T[]): T[]
```
对嵌套数组进行扁平化处理并去除重复元素。
示例:
```typescript
flattenAndUniq([
[1, 2],
[2, 3],
[3, 4],
]) // 返回: [1, 2, 3, 4]
```
## 数据映射
### 对象映射
```typescript
mapData(
mapper: [string, string][] | Record<string, string>,
data: Record<string, unknown> | Record<string, unknown>[],
options: MapperOption = { deep: true }
): Record<string, unknown> | Record<string, unknown>[]
```
根据映射表将对象或数组映射为新的数据结构。
参数:
- `mapper`: 映射表,可以是键值对数组或对象
- `data`: 要映射的数据,可以是对象或对象数组
- `options`: 映射选项
- `inherit`: 要继承的字段数组
- `deep`: 是否深度映射(默认 true
- `ignore`: 要忽略的字段数组
特点:
- 支持深度映射
- 支持字段继承和忽略
- 支持嵌套路径映射
- 自动处理数组数据
示例:
```typescript
const mapper = {
'user.name': 'userName',
'user.age': 'userAge',
}
const data = {
user: {
name: 'Alice',
age: 25,
},
}
mapData(mapper, data) // 返回: { userName: 'Alice', userAge: 25 }
// 使用继承选项
mapData(mapper, data, { inherit: ['user.name'] })
// 返回: { userName: 'Alice' }
// 使用忽略选项,或者在映射表中直接忽略,如果未启用
mapData(mapper, data, { ignore: ['user.age'] })
// 返回: { userName: 'Alice' }
```
### 生成映射表
```typescript
generateMapper(obj: Record<string, unknown>): [string, unknown][]
```
将对象的所有字段转换为小驼峰格式的映射表。
示例:
```typescript
const obj = {
user_name: 'name',
user_age: 'age',
}
generateMapper(obj) // 返回: [['userName', 'name'], ['userAge', 'age']]
```
## 特点
1. 使用 TypeScript 编写,提供完整的类型支持
2. 使用 Ramda.js 进行函数式编程
3. 所有函数都提供柯里化支持
4. 支持深层数据结构处理
5. 提供丰富的数据验证方法
## 注意事项
1. 所有函数都是纯函数,不会修改原始数据
2. 对象扁平化会处理所有层级的嵌套
3. 数组转对象时要确保指定的 key 在数组对象中存在
4. 数据映射的 inherit 和 ignore 选项不能同时使用
5. 映射表中的路径必须存在于源数据中

View File

@@ -0,0 +1,198 @@
# 日期处理工具函数文档
这个模块提供了一系列用于日期处理的实用工具函数。该模块使用 Ramda.js 进行函数式编程,并提供了完整的 TypeScript 类型支持。
## 目录
1. [日期格式化](#日期格式化)
2. [日期计算](#日期计算)
3. [日期判断](#日期判断)
4. [时间获取](#时间获取)
## 日期格式化
### 日期格式化
```typescript
formatDate(date: string | number | Date, format: string = 'YYYY-MM-DD HH:mm:ss'): string
```
将日期转换为指定格式的字符串。
参数:
- `date`: 日期字符串、时间戳或 Date 对象
- `format`: 格式化字符串,默认为 'YYYY-MM-DD HH:mm:ss'
支持的格式化占位符:
- YYYY: 年份
- MM: 月份01-12
- DD: 日期01-31
- HH: 小时00-23
- mm: 分钟00-59
- ss: 秒钟00-59
示例:
```typescript
formatDate(new Date(), 'YYYY-MM-DD') // 返回: '2024-02-27'
formatDate('2024-02-27 14:30:00', 'MM/DD HH:mm') // 返回: '02/27 14:30'
```
### 相对时间格式化
```typescript
formatRelativeTime(date: string | number | Date): string
```
将日期转换为相对时间描述。
返回格式:
- 1分钟内'刚刚'
- 1小时内'x分钟前'
- 24小时内'x小时前'
- 30天内'x天前'
- 超过30天显示具体日期YYYY-MM-DD
示例:
```typescript
formatRelativeTime(new Date()) // 返回: '刚刚'
formatRelativeTime(Date.now() - 3600000) // 返回: '1小时前'
```
## 日期计算
### 天数差计算
```typescript
getDaysDiff(startDate: string | number | Date, endDate: string | number | Date): number
getDaysDiffCurried(startDate: string | number | Date)(endDate: string | number | Date): number
```
计算两个日期之间的天数差。提供普通版本和柯里化版本。
示例:
```typescript
getDaysDiff('2024-02-01', '2024-02-27') // 返回: 26
const diffFromToday = getDaysDiffCurried(new Date())
diffFromToday('2024-03-27') // 返回: 30
```
### 添加天数
```typescript
addDays(days: number, date: string | number | Date): Date
addDaysCurried(days: number)(date: string | number | Date): Date
```
在指定日期上添加或减少天数。提供普通版本和柯里化版本。
示例:
```typescript
addDays(7, new Date()) // 返回: 7天后的日期
addDays(-7, new Date()) // 返回: 7天前的日期
```
## 日期判断
### 日期范围判断
```typescript
isDateInRange(date: string | number | Date, startDate: string | number | Date, endDate: string | number | Date): boolean
isDateInRangeCurried(date: string | number | Date)(startDate: string | number | Date)(endDate: string | number | Date): boolean
```
判断日期是否在指定范围内。提供普通版本和柯里化版本。
示例:
```typescript
isDateInRange('2024-02-27', '2024-02-01', '2024-03-01') // 返回: true
const checkDateRange = isDateInRangeCurried('2024-02-27')
checkDateRange('2024-02-01')('2024-03-01') // 返回: true
```
## 时间获取
### 获取一天的开始时间
```typescript
getStartOfDay(date: string | number | Date): Date
```
获取指定日期的开始时间00:00:00
示例:
```typescript
getStartOfDay('2024-02-27 15:30:00') // 返回: 2024-02-27 00:00:00
```
### 获取一天的结束时间
```typescript
getEndOfDay(date: string | number | Date): Date
```
获取指定日期的结束时间23:59:59.999)。
示例:
```typescript
getEndOfDay('2024-02-27 15:30:00') // 返回: 2024-02-27 23:59:59.999
```
### 获取星期几
```typescript
getDayOfWeek(date: string | number | Date): string
```
获取指定日期是星期几。
示例:
```typescript
getDayOfWeek('2024-02-27') // 返回: '星期二'
```
### 获取到期时间
```typescript
getDaysUntilExpiration(date: string | number | Date, expirationDate?: string | number | Date): string
```
获取距离到期时间的天数。
参数:
- `date`: 目标日期
- `expirationDate`: 到期日期,默认为当前时间
示例:
```typescript
getDaysUntilExpiration('2024-03-27') // 返回: '30天'
getDaysUntilExpiration('2024-01-27') // 返回: '已过期'
```
## 特点
1. 使用 TypeScript 编写,提供完整的类型支持
2. 使用 Ramda.js 进行函数式编程
3. 提供多个函数的柯里化版本
4. 支持多种日期输入格式
5. 丰富的日期处理功能
## 注意事项
1. 所有接收日期的函数都支持字符串、时间戳和 Date 对象作为输入
2. 日期格式化函数会自动补零,确保输出格式统一
3. 天数差计算会忽略时分秒,只计算日期差
4. 范围判断包含起始和结束日期
5. 使用柯里化函数时注意参数顺序

View File

@@ -0,0 +1,132 @@
# 加密解密工具函数文档
这个模块提供了一系列用于加密解密的实用工具函数。该模块使用 JSEncrypt 库实现 RSA 加密解密功能。
## 目录
1. [密钥对生成](#密钥对生成)
2. [RSA 加密](#rsa-加密)
3. [RSA 解密](#rsa-解密)
## 密钥对生成
### 生成 RSA 密钥对
```typescript
generateKeyPair(): { publicKey: string, privateKey: string }
```
生成一对 RSA 公私钥。
返回值:
- 包含公钥和私钥的对象
- `publicKey`: RSA 公钥
- `privateKey`: RSA 私钥
特点:
- 使用 2048 位密钥长度
- 返回标准 PEM 格式的密钥对
示例:
```typescript
const { publicKey, privateKey } = generateKeyPair()
// publicKey: '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----'
// privateKey: '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----'
```
## RSA 加密
### RSA 加密函数
```typescript
rsaEncrypt(str: string, publicKey: string): string
```
使用 RSA 公钥对字符串进行加密。
参数:
- `str`: 需要加密的字符串
- `publicKey`: RSA 公钥
返回值:
- 加密后的字符串
注意事项:
- 如果公钥长度小于 10将直接返回原字符串
- 使用 JSEncrypt 库进行加密操作
示例:
```typescript
const publicKey = '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----'
const encrypted = rsaEncrypt('hello world', publicKey)
```
## RSA 解密
### RSA 解密函数
```typescript
rsaDecrypt(str: string, privateKey: string): string
```
使用 RSA 私钥对加密字符串进行解密。
参数:
- `str`: 需要解密的字符串
- `privateKey`: RSA 私钥
返回值:
- 解密后的原始字符串
注意事项:
- 如果私钥长度小于 10将直接返回原字符串
- 使用 JSEncrypt 库进行解密操作
示例:
```typescript
const privateKey = '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----'
const decrypted = rsaDecrypt(encrypted, privateKey)
```
## 完整使用示例
```typescript
// 1. 生成密钥对
const { publicKey, privateKey } = generateKeyPair()
// 2. 使用公钥加密数据
const message = 'Hello, World!'
const encrypted = rsaEncrypt(message, publicKey)
// 3. 使用私钥解密数据
const decrypted = rsaDecrypt(encrypted, privateKey)
console.log(decrypted) // 输出: 'Hello, World!'
```
## 特点
1. 使用 TypeScript 编写,提供完整的类型支持
2. 基于 JSEncrypt 库实现 RSA 加密解密
3. 提供简单易用的 API
4. 内置密钥长度验证
5. 支持标准 RSA 密钥格式
6. 提供密钥对生成功能
## 注意事项
1. 使用前需要准备好 RSA 公私钥对,或使用 generateKeyPair 生成
2. 密钥必须符合标准 RSA 密钥格式
3. 建议在 HTTPS 环境下使用
4. 注意保护好私钥,不要在客户端存储
5. 加密数据有长度限制,取决于 RSA 密钥长度2048 位)

View File

@@ -0,0 +1,135 @@
# 随机数生成工具函数文档
这个模块提供了一系列用于生成随机数和随机字符串的工具函数。
## 目录
1. [随机数生成](#随机数生成)
2. [随机字符串生成](#随机字符串生成)
## 随机数生成
### 随机整数生成
```typescript
randomInt(min: number, max: number): number
```
生成指定范围内的随机整数。
参数:
- `min`: 最小值(包含)
- `max`: 最大值(包含)
示例:
```typescript
randomInt(1, 10) // 返回: 1-10 之间的随机整数
```
## 随机字符串生成
### 基础随机字符串
```typescript
randomChart(
length: number = 32,
options: {
isSpecial?: boolean;
isLower?: boolean;
isUpper?: boolean;
isNumber?: boolean;
} = {}
): string
```
生成指定长度的随机字符串。
参数:
- `length`: 字符串长度(默认 32
- `options`: 配置选项
- `isSpecial`: 是否包含特殊字符(默认 false
- `isLower`: 是否包含小写字母(默认 true
- `isUpper`: 是否包含大写字母(默认 true
- `isNumber`: 是否包含数字(默认 true
示例:
```typescript
// 生成默认随机字符串
randomChart() // 返回: 32位随机字符串
// 生成包含特殊字符的随机字符串
randomChart(16, { isSpecial: true })
// 仅生成数字和大写字母
randomChart(8, { isLower: false })
```
### 高级随机字符串
```typescript
randomChartWithMinLength(
length: number = 32,
options: {
minUpper?: number;
minLower?: number;
minNumber?: number;
minSpecial?: number;
}
): string
```
生成满足最小字符数要求的随机字符串。
参数:
- `length`: 字符串总长度(默认 32
- `options`: 最小字符数要求
- `minUpper`: 大写字母最小个数(默认 1
- `minLower`: 小写字母最小个数(默认 1
- `minNumber`: 数字最小个数(默认 1
- `minSpecial`: 特殊字符最小个数(默认 0
特点:
- 确保生成的字符串满足各类字符的最小数量要求
- 自动打乱字符顺序
- 支持特殊字符
示例:
```typescript
// 生成包含至少2个大写字母、2个小写字母、2个数字的16位随机字符串
randomChartWithMinLength(16, {
minUpper: 2,
minLower: 2,
minNumber: 2,
})
// 生成包含特殊字符的安全密码
randomChartWithMinLength(12, {
minUpper: 1,
minLower: 1,
minNumber: 1,
minSpecial: 1,
})
```
## 特点
1. 使用 TypeScript 编写,提供完整的类型支持
2. 支持灵活的字符组合配置
3. 提供基础和高级两种随机字符串生成方式
4. 支持最小字符数量要求
5. 自动字符顺序打乱
## 注意事项
1. 随机数范围包含最小值和最大值
2. 字符串生成会自动过滤掉易混淆的字符0oO1Ii
3. 最小字符数要求总和不能超过总长度
4. 特殊字符集合为 !@#$%^&\*?
5. 建议在需要安全性的场景使用 randomChartWithMinLength

View File

@@ -0,0 +1,139 @@
# 字符串处理工具函数文档
这个模块提供了一系列用于字符串处理的实用工具函数。该模块使用 Ramda.js 进行函数式编程,并提供了完整的 TypeScript 类型支持。
## 目录
1. [URL 处理](#url-处理)
2. [HTML 转义](#html-转义)
3. [命名格式转换](#命名格式转换)
## URL 处理
### URL 参数转对象
```typescript
urlToObject(url: string): Record<string, string>
urlToObjectCurried(url: string): Record<string, string>
```
将 URL 字符串中的查询参数转换为对象。提供普通版本和柯里化版本。
示例:
```typescript
const url = 'https://example.com?name=Alice&age=25'
urlToObject(url) // 返回: { name: 'Alice', age: '25' }
const parseUrl = urlToObjectCurried('https://example.com?name=Alice&age=25')
parseUrl // 返回: { name: 'Alice', age: '25' }
```
## HTML 转义
### HTML 字符转义
```typescript
htmlEscape(str: string, isReverse: boolean = false): string
```
对 HTML 字符串进行转义或反转义。
参数:
- `str`: 要转义的字符串
- `isReverse`: 是否进行反转义(默认 false
支持的转义字符:
- & -> &amp;
- < -> &lt;
- > -> &gt;
- " -> &quot;
- ' -> &apos;
示例:
```typescript
// 转义
htmlEscape('<div>Hello & World</div>')
// 返回: '&lt;div&gt;Hello &amp; World&lt;/div&gt;'
// 反转义
htmlEscape('&lt;div&gt;Hello &amp; World&lt;/div&gt;', true)
// 返回: '<div>Hello & World</div>'
```
## 命名格式转换
### 小驼峰转下划线
```typescript
camelToUnderline(str: string): string
```
将小驼峰命名转换为下划线命名。
示例:
```typescript
camelToUnderline('userName') // 返回: 'user_name'
```
### 下划线转小驼峰
```typescript
underlineToCamel(str: string): string
```
将下划线命名转换为小驼峰命名。
示例:
```typescript
underlineToCamel('user_name') // 返回: 'userName'
```
### 下划线转大驼峰
```typescript
underlineToBigCamel(str: string): string
```
将下划线命名转换为大驼峰命名。
示例:
```typescript
underlineToBigCamel('user_name') // 返回: 'UserName'
```
### 大驼峰转下划线
```typescript
bigCamelToUnderline(str: string): string
```
将大驼峰命名转换为下划线命名。
示例:
```typescript
bigCamelToUnderline('UserName') // 返回: 'user_name'
```
## 特点
1. 使用 TypeScript 编写,提供完整的类型支持
2. 使用 Ramda.js 进行函数式编程
3. 提供 URL 参数解析的柯里化版本
4. 支持 HTML 字符的双向转义
5. 提供完整的命名格式转换工具
## 注意事项
1. URL 解析使用标准的 URL API
2. HTML 转义支持最常用的五种字符
3. 命名格式转换保持原字符串的大小写特性
4. 柯里化函数便于函数组合
5. 所有函数都是纯函数,不会修改原始数据

View File

@@ -0,0 +1,247 @@
# 类型检查工具函数文档
这个模块提供了一系列用于数据类型检查的实用工具函数。该模块使用 Ramda.js 进行函数式编程,并提供了完整的 TypeScript 类型支持。
## 目录
1. [基础类型检查](#基础类型检查)
2. [复杂类型检查](#复杂类型检查)
3. [特殊类型检查](#特殊类型检查)
4. [类型获取](#类型获取)
## 基础类型检查
### 数字类型检查
```typescript
isNumber(value: unknown): value is number
```
检查值是否为数字类型。
示例:
```typescript
isNumber(123) // 返回: true
isNumber('123') // 返回: false
```
### 字符串类型检查
```typescript
isString(value: unknown): value is string
```
检查值是否为字符串类型。
示例:
```typescript
isString('hello') // 返回: true
isString(123) // 返回: false
```
### 布尔类型检查
```typescript
isBoolean(value: unknown): value is boolean
```
检查值是否为布尔类型。
示例:
```typescript
isBoolean(true) // 返回: true
isBoolean('true') // 返回: false
```
## 复杂类型检查
### 对象类型检查
```typescript
isObject(value: unknown): value is object
```
检查值是否为对象类型(不包括数组)。
示例:
```typescript
isObject({}) // 返回: true
isObject([]) // 返回: false
```
### 数组类型检查
```typescript
isArray(value: unknown): value is any[]
```
检查值是否为数组类型。
示例:
```typescript
isArray([1, 2, 3]) // 返回: true
isArray({ length: 3 }) // 返回: false
```
### 函数类型检查
```typescript
isFunction(value: unknown): value is Function
```
检查值是否为函数类型。
示例:
```typescript
isFunction(() => {}) // 返回: true
isFunction({}) // 返回: false
```
## 特殊类型检查
### Promise 类型检查
```typescript
isPromise(value: unknown): value is Promise<unknown>
```
检查值是否为 Promise 类型。
示例:
```typescript
isPromise(Promise.resolve()) // 返回: true
isPromise({ then: () => {} }) // 返回: false
```
### 正则表达式检查
```typescript
isRegExp(value: unknown): value is RegExp
```
检查值是否为正则表达式。
示例:
```typescript
isRegExp(/test/) // 返回: true
isRegExp('test') // 返回: false
```
### 日期类型检查
```typescript
isDate(value: unknown): value is Date
```
检查值是否为日期类型。
示例:
```typescript
isDate(new Date()) // 返回: true
isDate('2024-02-27') // 返回: false
```
### null 检查
```typescript
isNull(value: unknown): value is null
```
检查值是否为 null与 undefined 区分)。
示例:
```typescript
isNull(null) // 返回: true
isNull(undefined) // 返回: false
```
### undefined 检查
```typescript
isUndefined(value: unknown): value is undefined
```
检查值是否为 undefined。
示例:
```typescript
isUndefined(undefined) // 返回: true
isUndefined(null) // 返回: false
```
### 空值检查
```typescript
isEmpty(value: unknown): value is '' | any[] | object
```
检查值是否为空('', [], {}),但不包括 null 和 undefined。
示例:
```typescript
isEmpty('') // 返回: true
isEmpty([]) // 返回: true
isEmpty({}) // 返回: true
isEmpty(null) // 返回: false
```
## 类型获取
### 获取类型
```typescript
getType(value: unknown): string
```
获取值的类型字符串。
示例:
```typescript
getType(123) // 返回: 'Number'
getType('hello') // 返回: 'String'
getType(null) // 返回: 'Null'
```
### 类型比较
```typescript
isType<T>(type: string, value: unknown): value is T
```
检查值是否为指定类型。
示例:
```typescript
isType('Number', 123) // 返回: true
isType('String', 123) // 返回: false
```
## 特点
1. 使用 TypeScript 编写,提供完整的类型支持
2. 使用 Ramda.js 进行函数式编程
3. 所有检查函数都提供类型守卫
4. 支持复杂类型和特殊类型检查
5. 提供类型获取和比较功能
## 注意事项
1. 对象检查不包括数组类型
2. null 和 undefined 检查是严格区分的
3. 空值检查不包括 null 和 undefined
4. 类型获取返回首字母大写的类型字符串
5. 所有函数都是类型安全的

View File

@@ -0,0 +1,16 @@
import baseConfig from '@baota/eslint'
/** @type {import("eslint").Linter.Config[]} */
const config = [
// 基础配置,用于通用的 JavaScript/TypeScript 规则
...baseConfig,
// 项目特定的配置覆盖
{
files: ['**/*.{js,ts}'],
rules: {
// 在此处添加项目特定的规则覆盖
},
},
]
export default config

View File

@@ -0,0 +1,130 @@
{
"name": "@baota/utils",
"version": "1.0.0",
"type": "module",
"main": "./dist/browser.cjs",
"module": "./dist/browser.mjs",
"types": "./dist/browser.d.ts",
"files": [
"dist/**",
"dist"
],
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
"test": "vitest"
},
"exports": {
"./browser": {
"development": {
"types": "./src/browser.ts",
"import": "./src/browser.ts",
"require": "./src/browser.ts"
},
"default": {
"types": "./dist/browser.d.ts",
"import": "./dist/browser.mjs",
"require": "./dist/browser.cjs"
}
},
"./business": {
"development": {
"types": "./src/business.ts",
"import": "./src/business.ts",
"require": "./src/business.ts"
},
"default": {
"types": "./dist/business.d.ts",
"import": "./dist/business.mjs",
"require": "./dist/business.cjs"
}
},
"./data": {
"development": {
"types": "./src/data.ts",
"import": "./src/data.ts",
"require": "./src/data.ts"
},
"default": {
"types": "./dist/data.d.ts",
"import": "./dist/data.mjs",
"require": "./dist/data.cjs"
}
},
"./date": {
"development": {
"types": "./src/date.ts",
"import": "./src/date.ts",
"require": "./src/date.ts"
},
"default": {
"types": "./dist/date.d.ts",
"import": "./dist/date.mjs",
"require": "./dist/date.cjs"
}
},
"./encipher": {
"development": {
"types": "./src/encipher.ts",
"import": "./src/encipher.ts",
"require": "./src/encipher.ts"
},
"default": {
"types": "./dist/encipher.d.ts",
"import": "./dist/encipher.mjs",
"require": "./dist/encipher.cjs"
}
},
"./random": {
"development": {
"types": "./src/random.ts",
"import": "./src/random.ts",
"require": "./src/random.ts"
},
"default": {
"types": "./dist/random.d.ts",
"import": "./dist/random.mjs",
"require": "./dist/random.cjs"
}
},
"./string": {
"development": {
"types": "./src/string.ts",
"import": "./src/string.ts",
"require": "./src/string.ts"
},
"default": {
"types": "./dist/string.d.ts",
"import": "./dist/string.mjs",
"require": "./dist/string.cjs"
}
},
"./type": {
"development": {
"types": "./src/type.ts",
"import": "./src/type.ts",
"require": "./src/type.ts"
},
"default": {
"types": "./dist/type.d.ts",
"import": "./dist/type.mjs",
"require": "./dist/type.cjs"
}
}
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"jsencrypt": "^3.3.2",
"md5": "^2.3.0",
"ramda": "^0.30.1"
},
"devDependencies": {
"@baota/eslint": "workspace:*",
"@baota/typescript": "workspace:*",
"@baota/prettier": "workspace:*",
"vite": "^5.0.0",
"vite-plugin-dts": "^3.0.0"
}
}

View File

@@ -0,0 +1,3 @@
import prettierConfig from '@baota/prettier'
export default prettierConfig

View File

@@ -0,0 +1,585 @@
/**
* 文件定义:浏览器相关操作
*/
import * as R from 'ramda'
/* -------------- 1、浏览器相关操作 -------------- */
/**
* 获取当前页面 URL
*/
export const isHttps = (): boolean => window.location.protocol === 'https:'
/**
* 判断是否为开发环境
*/
export const isDev = (): boolean => process.env.NODE_ENV === 'development'
/**
* 获取浏览器及操作系统信息
* @returns {{ browser: string; os: string }} 浏览器和操作系统信息
*/
export const getBrowserOSInfo = (): { browser: string; os: string } => {
const ua = navigator.userAgent
type Rule = [(str: string) => boolean, () => string]
// 浏览器识别规则
const browserRules: Rule[] = [
[R.allPass([R.test(/Chrome/), R.complement(R.test(/Edg/))]), R.always('Chrome')],
[R.test(/Firefox/), R.always('Firefox')],
[R.allPass([R.test(/Safari/), R.complement(R.test(/Chrome/))]), R.always('Safari')],
[R.test(/Edg/), R.always('Edge')],
[R.T, R.always('Unknown')],
]
// 操作系统识别规则
const osRules: Rule[] = [
[R.test(/iPhone|iPad|iPod/), R.always('iOS')],
[R.test(/Android/), R.always('Android')],
[R.test(/Win/), R.always('Windows')],
[R.allPass([R.test(/Mac/), R.complement(R.test(/iPhone|iPad|iPod/))]), R.always('macOS')],
[R.test(/Linux/), R.always('Linux')],
[R.T, R.always('Unknown')],
]
return {
browser: R.cond(browserRules)(ua),
os: R.cond(osRules)(ua),
}
}
/**
* 获取屏幕信息,分辨率、缩放比例
*/
export const getScreenInfo = (): {
resolution: string
scale: number
} => {
const resolution = `${window.screen.width}x${window.screen.height}`
const scale = window.devicePixelRatio
return { resolution, scale }
}
/* -------------- 2、浏览器缓存相关操作 -------------- */
/**
* 强制刷新页面并清理所有缓存
* 清除 Cache API、localStorage 和 sessionStorage 后刷新
*/
export const forceRefresh = () => {
clearBrowserCache()
// window.location.reload()
}
/**
* 获取 URL 参数
* @param name 参数名
*/
export const getUrlParam = (name: string): string | null => {
const params = new URLSearchParams(window.location.search)
return params.get(name)
}
/**
* 柯里化版本的getUrlParam
*/
export const getUrlParamCurried: {
(name: string): string | null
} = R.curry(getUrlParam)
/**
* Cookie 操作辅助函数:根据 HTTPS 协议增加前缀
* @param key cookie 键名
*/
export const cookiePrefixKey = (key: string): string =>
R.ifElse(R.always(isHttps()), (k: string) => `https_${k}`, R.identity)(key)
/**
* 设置 Cookie
* @param {string} key 键名
* @param {string} value 值
* @param {number} days 过期天数(可选)
*/
export const setCookie = (key: string, value: string, days?: number): void => {
const prefixedKey = cookiePrefixKey(key)
// 获取过期时间
const getExpires = (days?: number): string => {
if (!days) return ''
const date = new Date()
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
return `; expires=${date.toUTCString()}`
}
const expires = getExpires(days)
document.cookie = `${prefixedKey}=${encodeURIComponent(value)}${expires}; path=/`
}
/**
* 柯里化版本的setCookie
*/
export const setCookieCurried: {
(key: string, value: string, days?: number): void
(key: string): (value: string, days?: number) => void
(key: string, value: string): (days?: number) => void
} = R.curry(setCookie)
/**
* 获取 Cookie
* @param key 键名
*/
export const getCookie = (key: string, isPrefixKey: boolean = true): string | null => {
const prefixedKey = isPrefixKey ? cookiePrefixKey(key) : key
const nameEQ = `${prefixedKey}=`
const cookies = document.cookie.split(';').map((c) => c.trim())
const cookie = cookies.find((c) => c.startsWith(nameEQ))
if (cookie) {
return decodeURIComponent(cookie.substring(nameEQ.length))
}
return null
}
/**
* 柯里化版本的getCookie
*/
export const getCookieCurried: {
(key: string): string | null
} = R.curry(getCookie)
/**
* 删除 Cookie
* @param key 键名
*/
export const deleteCookie = (key: string): void => {
// 设置过期时间为负值,即删除 Cookie
setCookie(key, '', -1)
console.log(document.cookie)
}
/**
* 清空 Cookie
*/
export const clearCookie = (): void => {
const cookies = document.cookie.split(';').map((c) => c.trim()) // 获取所有 Cookie
cookies.forEach((c) => {
const [key] = c.split('=')
if (key) {
// 通过设置过期时间为过去来删除cookie
document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
}
})
}
/**
* 设置存储增强(支持自动序列化)
*/
export const setStorageItem = (key: string, value: any, storage: Storage): void => {
const serializedValue = JSON.stringify(value)
storage.setItem(key, serializedValue)
}
/**
* 柯里化版本的setStorageItem
*/
export const setStorageItemCurried: {
(key: string, value: any, storage: Storage): void
(key: string): (value: any, storage: Storage) => void
(key: string, value: any): (storage: Storage) => void
} = R.curry(setStorageItem)
/**
* 获取存储增强(支持自动反序列化)
*/
export const getStorageItem = (key: string, storage: Storage): any => {
const value = storage.getItem(key)
return value ? JSON.parse(value) : null
}
/**
* 柯里化版本的getStorageItem
*/
export const getStorageItemCurried: {
(key: string, storage: Storage): any
(key: string): (storage: Storage) => any
} = R.curry(getStorageItem)
/**
* 删除存储
* @param key 键名
* @param storage 存储类型(可选)
*/
export const removeStorageItem = (key: string, storage: Storage = localStorage): void => storage.removeItem(key)
/**
* 设置 sessionStorage 数据
* @param key 键名
* @param value 值
*/
export const setSessionItem = (key: string, value: any): void => setStorageItem(key, value, sessionStorage)
/**
* 获取 sessionStorage 数据
* @param key 键名
*/
export const getSessionItem = (key: string): any => getStorageItem(key, sessionStorage)
/**
* 删除 sessionStorage 数据
* @param key 键名
*/
export const removeSessionItem = (key: string): void => sessionStorage.removeItem(key)
/**
* 清空 sessionStorage 中所有数据
*/
export const clearSession = (): void => sessionStorage.clear()
/**
* 设置 localStorage 数据
* @param key 键名
* @param value 值
*/
export const setLocalItem = (key: string, value: any): void => setStorageItem(key, value, localStorage)
/**
* 获取 localStorage 数据
* @param key 键名
*/
export const getLocalItem = (key: string): any => getStorageItem(key, localStorage)
/**
* 删除 localStorage 数据
* @param key 键名
*/
export const removeLocalItem = (key: string): void => localStorage.removeItem(key)
/**
* 清空 localStorage 中所有数据
*/
export const clearLocal = (): void => localStorage.clear()
/**
* 清空浏览器缓存
*/
export const clearBrowserCache = (): void => {
clearSession()
clearLocal()
clearCookie()
}
/**
* 创建过期时间的存储支持sessionStorage和localStorage
* @param key 键名
* @param value 值
* @param time 过期时间可选支持new Date()、时间戳、时间字符串
* @param storage 存储类型(可选)
*/
export const setExpiredStorageItem = (
key: string,
value: any,
time?: Date | number | string,
storage: Storage = localStorage,
): void => {
// 如果没有设置过期时间,直接存储值
if (!time) {
setStorageItem(key, value, storage)
return
}
// 转换过期时间为时间戳
let expires: number
if (time instanceof Date) {
expires = time.getTime()
} else if (typeof time === 'number') {
expires = time
} else {
expires = new Date(time).getTime()
}
// 存储数据和过期时间
const data = {
value,
expires,
}
setStorageItem(key, data, storage)
}
/**
* 获取过期时间的存储
* @param key 键名
*/
export const getExpiredStorageItem = (key: string, storage: Storage = localStorage): any => {
const data = getStorageItem(key, storage)
if (!data) return null
// 检查是否过期
if (data.expires && data.expires < Date.now()) {
removeStorageItem(key, storage)
return null
}
}
/* -------------- 3、IndexedDB 相关操作 -------------- */
/**
* IndexedDB 相关类型定义
* @interface IndexedDBConfig
* @description 数据库配置接口,用于定义数据库的基本结构
* @property {string} dbName - 数据库名称
* @property {number} version - 数据库版本号,用于数据库升级
* @property {Object} stores - 存储对象配置key 为存储对象名称
*/
export interface IndexedDBConfig {
dbName: string
version: number
stores: {
[key: string]: {
/** 主键路径,用于唯一标识记录 */
keyPath: string
/** 索引配置数组,用于优化查询性能 */
indexes?: Array<{
/** 索引名称 */
name: string
/** 索引的键路径 */
keyPath: string
/** 索引选项,如是否唯一等 */
options?: IDBIndexParameters
}>
}
}
}
/**
* IndexedDB 管理类
* @class IndexedDBManager
* @description 提供 IndexedDB 数据库操作的统一接口,支持异步操作和类型安全
*/
export class IndexedDBManager {
/** 数据库连接实例 */
private db: IDBDatabase | null = null
/** 数据库配置信息 */
private config: IndexedDBConfig
/**
* 构造函数
* @param config 数据库配置对象
*/
constructor(config: IndexedDBConfig) {
this.config = config
}
/**
* 统一的事件处理器
* @description 处理 IDBRequest 的成功和错误事件
* @template T 返回数据类型
* @param request IDBRequest 实例
* @returns Promise<T> 返回处理结果
*/
private handleRequest<T>(request: IDBRequest<T>): Promise<T> {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}
/**
* 获取事务和对象仓库
* @description 创建事务并获取对象仓库
* @param storeName 仓库名称
* @param mode 事务模式
* @returns 包含事务和对象仓库的对象
*/
private async getTransactionAndStore(
storeName: string,
mode: IDBTransactionMode = 'readonly',
): Promise<{
transaction: IDBTransaction
store: IDBObjectStore
}> {
await this.connect()
const transaction = this.db!.transaction(storeName, mode)
const store = transaction.objectStore(storeName)
return { transaction, store }
}
/**
* 初始化数据库连接
* @description 创建或打开数据库连接,如果数据库不存在则自动创建
* @returns {Promise<IDBDatabase>} 返回数据库连接实例
* @throws {Error} 连接失败时抛出错误
*/
async connect(): Promise<IDBDatabase> {
if (this.db) return this.db
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.config.dbName, this.config.version)
request.onerror = () => reject(request.error)
request.onsuccess = () => {
this.db = request.result
resolve(request.result)
}
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result
Object.entries(this.config.stores).forEach(([storeName, storeConfig]) => {
if (!db.objectStoreNames.contains(storeName)) {
const store = db.createObjectStore(storeName, { keyPath: storeConfig.keyPath })
storeConfig.indexes?.forEach((index) => {
store.createIndex(index.name, index.keyPath, index.options)
})
}
})
}
})
}
/**
* 添加数据
* @description 向指定的对象仓库添加新数据
* @template T 数据类型
* @param {string} storeName 仓库名称
* @param {T} data 要添加的数据
* @returns {Promise<IDBValidKey>} 返回新添加数据的主键
*/
async add<T>(storeName: string, data: T): Promise<IDBValidKey> {
const { store } = await this.getTransactionAndStore(storeName, 'readwrite')
return this.handleRequest(store.add(data))
}
/**
* 更新数据
* @description 更新指定对象仓库中的数据,如果数据不存在则添加
* @template T 数据类型
* @param {string} storeName 仓库名称
* @param {T} data 要更新的数据
* @returns {Promise<IDBValidKey>} 返回更新数据的主键
*/
async put<T>(storeName: string, data: T): Promise<IDBValidKey> {
const { store } = await this.getTransactionAndStore(storeName, 'readwrite')
return this.handleRequest(store.put(data))
}
/**
* 删除数据
* @description 从指定对象仓库中删除数据
* @param {string} storeName 仓库名称
* @param {IDBValidKey} key 要删除数据的主键
* @returns {Promise<void>} 删除成功时解析
*/
async delete(storeName: string, key: IDBValidKey): Promise<void> {
const { store } = await this.getTransactionAndStore(storeName, 'readwrite')
return this.handleRequest(store.delete(key))
}
/**
* 通过主键获取数据
* @description 从指定对象仓库中获取指定主键的数据
* @template T 返回数据类型
* @param {string} storeName 仓库名称
* @param {IDBValidKey} key 主键值
* @returns {Promise<T | undefined>} 返回查询到的数据
*/
async get<T>(storeName: string, key: IDBValidKey): Promise<T | undefined> {
const { store } = await this.getTransactionAndStore(storeName)
return this.handleRequest(store.get(key))
}
/**
* 通过索引查询数据
* @description 使用索引从指定对象仓库中查询数据
* @template T 返回数据类型
* @param {string} storeName 仓库名称
* @param {string} indexName 索引名称
* @param {IDBValidKey} key 索引值
* @returns {Promise<T | undefined>} 返回查询到的数据
*/
async getByIndex<T>(storeName: string, indexName: string, key: IDBValidKey): Promise<T | undefined> {
const { store } = await this.getTransactionAndStore(storeName)
const index = store.index(indexName)
return this.handleRequest(index.get(key))
}
/**
* 获取所有数据
* @description 获取指定对象仓库中的所有数据
* @template T 返回数据类型
* @param {string} storeName 仓库名称
* @returns {Promise<T[]>} 返回所有数据的数组
*/
async getAll<T>(storeName: string): Promise<T[]> {
const { store } = await this.getTransactionAndStore(storeName)
return this.handleRequest(store.getAll())
}
/**
* 使用游标遍历数据
* @description 使用游标遍历对象仓库中的数据
* @template T 数据类型
* @param {string} storeName 仓库名称
* @param {(item: T) => void} callback 处理每条数据的回调函数
* @returns {Promise<void>}
*/
async forEach<T>(storeName: string, callback: (item: T) => void): Promise<void> {
const { store } = await this.getTransactionAndStore(storeName)
return new Promise((resolve, reject) => {
const request = store.openCursor()
request.onerror = () => reject(request.error)
request.onsuccess = () => {
const cursor = request.result
if (cursor) {
callback(cursor.value)
cursor.continue()
} else {
resolve()
}
}
})
}
/**
* 批量添加数据
* @description 向指定的对象仓库批量添加数据
* @template T 数据类型
* @param {string} storeName 仓库名称
* @param {T[]} items 要添加的数据数组
* @returns {Promise<void>}
*/
async addBatch<T>(storeName: string, items: T[]): Promise<void> {
const { store } = await this.getTransactionAndStore(storeName, 'readwrite')
return new Promise((resolve, reject) => {
try {
items.forEach((item) => store.add(item))
resolve()
} catch (error) {
reject(error)
}
})
}
/**
* 清空对象仓库
* @description 删除指定对象仓库中的所有数据
* @param {string} storeName 仓库名称
* @returns {Promise<void>}
*/
async clear(storeName: string): Promise<void> {
const { store } = await this.getTransactionAndStore(storeName, 'readwrite')
return this.handleRequest(store.clear())
}
/**
* 关闭数据库连接
* @description 安全地关闭数据库连接,释放资源
*/
close(): void {
if (this.db) {
this.db.close()
this.db = null
}
}
}

View File

@@ -0,0 +1,314 @@
/**
* 文件定义:业务处理
*/
import * as R from 'ramda'
import { isArray } from './type'
/* -------------- 1、常用正则验证 -------------- */
// 常量定义区域
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const PHONE_REGEX = /^1[3-9]\d{9}$/
const ID_CARD_REGEX = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/
const URL_REGEX = /^((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+/
const DOMAIN_REGEX = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/
// 增强版域名正则表达式 - 支持国际化域名和更多顶级域名
const ENHANCED_DOMAIN_REGEX =
/^(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)|(?:\*))\.)+(?:[a-zA-Z\u00a1-\uffff]{2,}|xn--[a-zA-Z0-9]+)$/
// 通配符域名正则表达式 - 支持通配符域名格式 (如 *.example.com)
const WILDCARD_DOMAIN_REGEX = /^\*\.(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/
// IPv4正则表达式 - 更精确的数字范围
const IPV4_SEGMENT = '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])'
const IPV4_REGEX = new RegExp(`^${IPV4_SEGMENT}\\.${IPV4_SEGMENT}\\.${IPV4_SEGMENT}\\.${IPV4_SEGMENT}$`)
// IPv6正则表达式 - 更精确的十六进制表示
const IPV6_HEX_4DIGIT = '[0-9A-Fa-f]{1,4}'
const IPV6_REGEX = new RegExp(
[
// 标准IPv6地址
`^(${IPV6_HEX_4DIGIT}:){7}${IPV6_HEX_4DIGIT}$`,
// 压缩形式
`^(${IPV6_HEX_4DIGIT}:){1,7}:$`,
'^:((:[0-9A-Fa-f]{1,4}){1,7}|:)$',
// 混合形式
`^(${IPV6_HEX_4DIGIT}:){1,6}:${IPV6_HEX_4DIGIT}$`,
`^(${IPV6_HEX_4DIGIT}:){1,5}(:${IPV6_HEX_4DIGIT}){1,2}$`,
`^(${IPV6_HEX_4DIGIT}:){1,4}(:${IPV6_HEX_4DIGIT}){1,3}$`,
`^(${IPV6_HEX_4DIGIT}:){1,3}(:${IPV6_HEX_4DIGIT}){1,4}$`,
`^(${IPV6_HEX_4DIGIT}:){1,2}(:${IPV6_HEX_4DIGIT}){1,5}$`,
`^${IPV6_HEX_4DIGIT}:(:${IPV6_HEX_4DIGIT}){1,6}$`,
// 特殊形式
`^fe80:(:[0-9A-Fa-f]{1,4}){0,4}%[0-9A-Za-z]{1,}$`,
// IPv4映射到IPv6
`^::((ffff(:0{1,4})?:)?${IPV4_SEGMENT}\\.${IPV4_SEGMENT}\\.${IPV4_SEGMENT}\\.${IPV4_SEGMENT})$`,
`^(${IPV6_HEX_4DIGIT}:){1,4}:${IPV4_SEGMENT}\\.${IPV4_SEGMENT}\\.${IPV4_SEGMENT}\\.${IPV4_SEGMENT}$`,
].join('|'),
)
// IP段正则表达式
const IPS_REGEX = new RegExp(
`^${IPV4_SEGMENT}\\.${IPV4_SEGMENT}\\.${IPV4_SEGMENT}\\.${IPV4_SEGMENT}(\\/([1-2][0-9]|3[0-2]|[1-9]))?$`,
)
// MAC地址正则表达式
const MAC_REGEX = /^([0-9A-Fa-f]{2}-){5}[0-9A-Fa-f]{2}$/
// 中文正则表达式
const CHINESE_REGEX = /^[\u4e00-\u9fa5]+$/
// 端口正则表达式 - 更精确的数字范围
const PORT_REGEX = /^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/
/**
* 判断是否为邮箱
* @param {string} email - 要判断的邮箱
* @returns {boolean} 如果邮箱是有效的,则返回 true否则返回 false
*/
export const isEmail = R.test(EMAIL_REGEX)
/**
* 判断是否为手机号
* @param {string} phone - 要判断的手机号
* @returns {boolean} 如果手机号是有效的,则返回 true否则返回 false
*/
export const isPhone = R.test(PHONE_REGEX)
/**
* 判断是否为身份证号
* @param {string} idCard - 要判断的身份证号
* @returns {boolean} 如果身份证号是有效的,则返回 true否则返 false
*/
export const isIdCard = R.test(ID_CARD_REGEX)
/**
* 判断是否为URL
* @param {string} url - 要判断的url
* @returns {boolean} 如果url是有效的则返回 true否则返回 false
*/
export const isUrl = R.test(URL_REGEX)
/**
* 判断是否为IPv4地址
* @param {string} ip - 要判断的IP地址
* @returns {boolean} 如果是有效的IPv4地址则返回 true否则返回 false
*/
export const isIpv4 = R.test(IPV4_REGEX)
/**
* 判断是否为IPv6地址
* @param {string} ip - 要判断的IP地址
* @returns {boolean} 如果是有效的IPv6地址则返回 true否则返回 false
*/
export const isIpv6 = R.test(IPV6_REGEX)
/**
* 判断是否为IP地址IPv4或IPv6
* @param {string} ip - 要判断的IP地址
* @returns {boolean} 如果IP地址是有效的则返回 true否则返回 false
*/
export const isIp = (ip: string): boolean => isIpv4(ip) || isIpv6(ip)
/**
* 判断是否为IP段
* @param {string} ips - 要判断的IP段
* @returns {boolean} 如果IP段是有效的则返回 true否则返回 false
*/
export const isIps = R.test(IPS_REGEX)
/**
* 判断端口
* @param {string} port - 判断端口
* @returns {boolean} 如果端口是有效的,则返回 true否则返回 false
*/
export const isPort = R.test(PORT_REGEX)
/**
* 判断是否为MAC地址
* @param {string} mac - 要判断的MAC地址
* @returns {boolean} 如果MAC地址是有效的则返回 true否则返回 false
*/
export const isMac = R.test(MAC_REGEX)
/**
* 判断是否为中文
* @param {string} str - 要判断的字符串
* @returns {boolean} 如果字符串是中文,则返回 true否则返回 false
*/
export const isChinese = R.test(CHINESE_REGEX)
/**
* 判断是否为域名
* @param {string} domain - 要判断的域名
* @returns {boolean} 如果域名是有效的,则返回 true否则返回 false
*/
export const isDomain = R.test(DOMAIN_REGEX)
/**
* 判断是否为域名(增强版)
* @param {string} domain - 要判断的域名,支持国际化域名和更多顶级域名
* @returns {boolean} 如果域名是有效的,则返回 true否则返回 false
*/
export const isEnhancedDomain = R.test(ENHANCED_DOMAIN_REGEX)
/**
* 判断是否为通配符域名
* @param {string} domain - 要判断的通配符域名
* @returns {boolean} 如果通配符域名是有效的,则返回 true否则返回 false
*/
export const isWildcardDomain = R.test(WILDCARD_DOMAIN_REGEX)
/**
* 判断域名组,通过特定字符串分割
* @param {string} domain - 要判断的域名
* @param {string} separator - 分割符
* @returns {boolean} 如果域名组是有效的,则返回 true否则返回 false
*/
export const isDomainGroup = (domain: string, separator: string = ',') => {
return R.all(
R.equals(true),
R.map(
(item: string) => isDomain(item) || isWildcardDomain(item) || isEnhancedDomain(item),
R.split(separator, domain),
),
)
}
/* -------------- 2、常用业务操作 -------------- */
/**
* 手机号加密
* @param {string} phone - 要加密的手机号
* @returns {string} 加密后的手机号
*/
export const encryptPhone = (phone: string): string => phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
/**
* 身份证号加密
* @param {string} idCard - 要加密的身份证号18位最后一位可以是X
* @returns {string} 加密后的身份证号
*/
export const encryptIdCard = (idCard: string): string => idCard.replace(/(\d{6})\d{8}([\dXx]{4})/, '$1****$2')
/**
* 版本号比较
* @param {string} version1 - 版本号1
* @param {string} version2 - 版本号2
* @returns {number} 如果版本号1大于版本号2则返回1如果版本号1小于版本号2则返回-1如果版本号1等于版本号2则返回0
*/
export const compareVersion = (version1: string, version2: string): number => {
// 使用Ramda的pipe函数组合操作
const parseVersion = R.pipe(
R.split('.'),
R.map((v: string) => parseInt(v || '0', 10)),
)
const v1 = parseVersion(version1) // 解析版本号1
const v2 = parseVersion(version2) // 解析版本号2
// 确保两个数组长度相同
const len = Math.max(v1.length, v2.length)
// 使用Ramda的repeat和take函数来填充数组
const paddedV1 = R.concat(v1, R.repeat(0, len - v1.length))
const paddedV2 = R.concat(v2, R.repeat(0, len - v2.length))
// 使用Ramda的zipWith比较每个版本号段
const comparisons = R.zipWith((a: number, b: number) => (a === b ? 0 : a > b ? 1 : -1), paddedV1, paddedV2)
// 找到第一个非零的比较结果
const result = R.find(R.complement(R.equals(0)), comparisons)
return result ?? 0
}
/**
* 字节转换
* @param {number} bytes - 要转换的字节数
* @param {number} [fixed=2] - 保留小数位数
* @param {boolean} [isUnit=true] - 是否显示单位
* @param {string} [endUnit=''] - 指定结束单位,如果指定则转换到该单位为止
* @returns {string} 转换后的字节数
*/
export const formatBytes = (bytes: number, fixed: number = 2, isUnit: boolean = true, endUnit: string = ''): string => {
if (bytes === 0) return isUnit ? '0 B' : '0'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
const c = 1024
// 使用Ramda的递归函数进行单位转换
const convert = (value: number, unitIndex: number): string => {
const unit = units[unitIndex]
const formattedValue = unitIndex === 0 || fixed === 0 ? Math.round(value).toString() : value.toFixed(fixed)
// 如果指定了结束单位或者已经是最小单位
if ((endUnit && unit === endUnit) || value < c || unitIndex >= units.length - 1) {
return isUnit ? `${formattedValue} ${unit}` : formattedValue
}
// 继续转换到下一个单位
return convert(value / c, unitIndex + 1)
}
return convert(bytes, 0)
}
/**
* 柯里化版本的formatBytes
* @param {number} bytes - 要转换的字节数
* @param {number} [fixed=2] - 保留小数位数
* @param {boolean} [isUnit=true] - 是否显示单位
* @param {string} [endUnit=''] - 指定结束单位,如果指定则转换到该单位为止
* @returns {string} 转换后的字节数
*/
export const formatBytesCurried: {
(bytes: number, fixed: number, isUnit: boolean, endUnit: string): string
(bytes: number): (fixed?: number, isUnit?: boolean, endUnit?: string) => string
(bytes: number, fixed: number): (isUnit?: boolean, endUnit?: string) => string
(bytes: number, fixed: number, isUnit: boolean): (endUnit?: string) => string
} = R.curry(formatBytes)
/**
* 分页字符串转换
* @param {string} page - 分页字符串
* @returns {string} 转换后的分页字符串
*/
export const formatPage = (page: string): number => {
const newPage = page.match(/class='Pcount'>共([0-9]*)条</)
if (isArray(newPage) && newPage.length >= 2) return Number(newPage[1])
return 0
}
/* -------------- 3、代理函数 -------------- */
export type ProxyConfig = {
requestTime: number
requestToken: string
request_time: number
request_token: string
}
/**
* 代理配置,仅在开发环境生效
* @param {string} proxyKey - 代理密钥
* @param {string} usage 使用场景 "query" | "params"
* @returns {Object} 返回对象包含 request_time 和 request_token
*/
export const getProxyConfig = async (proxyKey: string, usage: 'query' | 'params' = 'params') => {
const md5 = await import('md5')
const request_time = Date.now()
const request_token = md5.default(String(request_time).concat(md5.default(proxyKey)))
if (usage === 'params') {
return { request_time, request_token, requestTime: request_time, requestToken: request_token }
}
return `request_time=${request_time}&request_token=${request_token}`
}
/** -------------- 4、接口缓存配置 -------------- */
/**
* 接口缓存配置
* @param {function} method - 接口请求方法
* @param {string} params - 接口请求参数
* @param {Record<string, any>} options - 接口请求配置
* @returns {string} 返回数据
*/
export const getCacheConfig = (method: Function, params: string, options: Record<string, any> = {}) => {
console.log(method, params, options)
}

View File

@@ -0,0 +1,300 @@
/**
* 文件定义:数据处理方法
* 包含1、数据类型检查。2、数据转换。3、日期处理。4、数据校验。5、数据过滤与重组。6、特殊场景处理
*/
import * as R from 'ramda'
// =============== 数据转换 ===============
/**
* 将对象的所有值转换为字符串
* @param {Record<string, any>} obj - 要转换的对象
* @returns {Record<string, string>} 转换后的对象
*/
export const objectToString = R.map(String)
/**
* 将数组转换为对象,使用指定的 key
* @param {string} key - 要转换的 key
* @param {Record<string, any>[]} array - 要转换的数组
* @returns {Record<string, Record<string, any>>} 转换后的对象
*/
export const arrayToObject = R.curry((key: string, array: Record<string, any>[]) => R.indexBy(R.prop(key), array)) as <
T extends Record<string, any>,
>(
key: string,
array: T[],
) => Record<string, T>
/**
* 深度扁平化对象(建议深度嵌套的对象使用)
* @param {Record<string, any>} obj - 要扁平化的对象
* @returns {Record<string, any>} 扁平化后的对象
*/
export const flattenObject = (obj: Record<string, unknown>): Record<string, unknown> => {
const result: Record<string, unknown> = {}
const flatten = (obj: Record<string, any>, prefix: string = '') => {
for (const key in obj) {
const value = obj[key]
const newKey = prefix ? `${prefix}.${key}` : key
if (value && typeof value === 'object' && !Array.isArray(value)) {
flatten(value, newKey)
} else {
result[newKey] = value
}
}
}
flatten(obj)
return result
}
/**
* 验证字符串是否符合正则表达式
* @param {RegExp} pattern - 要验证的正则表达式
* @param {string} str - 要验证的字符串
* @returns {boolean} 如果字符串符合正则表达式,则返回 true否则返回 false
*/
export const matchesPattern = R.curry((pattern: RegExp, str: string) => R.test(pattern, str)) as <T extends RegExp>(
pattern: T,
str: string,
) => boolean
/**
* 验证对象是否包含所有必需的键
* @param {Record<string, any>} obj - 要验证的对象
* @param {string[]} requiredKeys - 要验证的键
* @returns {boolean} 如果对象包含所有必需的键,则返回 true否则返回 false
*/
export const hasRequiredKeys = R.curry((obj: Record<string, unknown>, requiredKeys: string[]) =>
R.all(R.flip(R.has)(obj), requiredKeys),
) as {
(obj: Record<string, unknown>): (requiredKeys: string[]) => boolean
(obj: Record<string, unknown>, requiredKeys: string[]): boolean
}
// ... existing code ...
/**
* 验证值是否在指定范围内
* @param {number} min - 最小值
* @param {number} max - 最大值
* @param {number} value - 要验证的值
* @returns {boolean} 如果值在指定范围内,则返回 true否则返回 false
*/
export const isInRange = R.curry((min: number, max: number, value: number) =>
R.both(R.gte(R.__, min), R.lte(R.__, max))(value),
) as <T extends number>(min: T, max: T, value: T) => boolean
// =============== 数据过滤与重组 ===============
/**
* 根据条件过滤对象的属性
* @param {Function} predicate - 要过滤的条件
* @param {Record<string, any>} obj - 要过滤的对象
* @returns {Record<string, any>} 过滤后的对象
*/
export const filterObject = R.curry(
<T extends Record<string, any>>(predicate: (value: T[keyof T]) => boolean, obj: T) =>
Object.fromEntries(Object.entries(obj).filter(([_, value]) => predicate(value))),
) as {
<T extends Record<string, any>>(predicate: (value: T[keyof T]) => boolean): (obj: T) => Partial<T>
<T extends Record<string, any>>(predicate: (value: T[keyof T]) => boolean, obj: T): Partial<T>
}
/**
* 按照指定的键对数组进行分组
* @param {string} key - 要分组的键
* @param {Record<string, any>[]} array - 要分组的数组
* @returns {Record<string, Record<string, any>[]>} 分组后的对象
*/
export const groupByKey = R.curry(<T extends Record<string, any>>(key: string, array: T[]) =>
R.groupBy(R.prop(key), array),
) as <T extends Record<string, any>>(key: string, array: T[]) => Record<string, T[]>
/**
* 从对象数组中提取指定的键值
* @param {string[]} path - 要提取的键
* @param {Record<string, any>[]} list - 要提取的对象数组
* @returns {Record<string, any>[]} 提取后的对象数组
*/
export const pluckDeep = R.curry(<T>(path: string[], list: T[]) => R.map(R.path(path), list)) as <
T extends Record<string, any>,
>(
path: string[],
list: T[],
) => T[]
/**
* 对嵌套数组进行扁平化和去重
* @param {any[]} array - 要扁平化和去重的数组
* @returns {any[]} 扁平化和去重后的数组
*/
export const flattenAndUniq = R.pipe(R.flatten, R.uniq) as <T>(array: T[]) => T[]
// =============== 数据映射 ===============
type MapperOption = {
inherit?: string[] // 继承字段
deep?: boolean // 深度映射
ignore?: string[] // 忽略字段
}
type MapperType = [string, string][] | Record<string, string>
type DataType = Record<string, unknown> | Record<string, unknown>[]
/**
* 对象/数组映射,根据映射表,将数组或对象映射为新的对象和数组
* 支持继承/过滤,通过参数继承/过滤,选取自己需要的数据
* 增加异常处理,如果值不存在,则抛出异常。
* 返回新的对象/数组
*/
export const mapData = (mapper: MapperType, data: DataType, options: MapperOption = { deep: true }): DataType => {
const { inherit, deep, ignore } = options
// 验证 inherit 和 ignore 不能同时使用
if (inherit && ignore) {
throw new Error('inherit 和 ignore 选项不能同时使用')
}
// 将 mapper 转换为对象形式
const mapperObj = Array.isArray(mapper)
? mapper.reduce<Record<string, string>>((acc, [key, value]) => ({ ...acc, [key]: value }), {})
: mapper
// 处理数组
if (Array.isArray(data)) {
return data.map((item) => mapData(mapperObj, item, options) as Record<string, unknown>)
}
// 处理对象
if (typeof data === 'object' && data !== null) {
// 根据选项过滤 mapper
let finalMapper = { ...mapperObj }
if (inherit) {
finalMapper = Object.entries(mapperObj)
.filter(([key]) => inherit.includes(key))
.reduce<Record<string, string>>((acc, [key, value]) => ({ ...acc, [key]: value }), {})
} else if (ignore) {
finalMapper = Object.entries(mapperObj)
.filter(([key]) => !ignore.includes(key))
.reduce<Record<string, string>>((acc, [key, value]) => ({ ...acc, [key]: value }), {})
}
return Object.entries(finalMapper).reduce<Record<string, unknown>>((result, [sourceKey, targetKey]) => {
// 处理嵌套路径
const value = sourceKey.split('.').reduce<unknown>((obj, key) => {
if (obj === undefined || obj === null) {
throw new Error(`映射键 "${sourceKey}" 不存在于源数据中`)
}
return (obj as Record<string, unknown>)[key]
}, data)
// 处理值不存在的情况
if (value === undefined) {
throw new Error(`映射键 "${sourceKey}" 的值不存在`)
}
// 处理深度映射
if (deep && typeof value === 'object' && value !== null) {
const nestedMapper = Object.entries(mapperObj)
.filter(([key]) => key.startsWith(`${sourceKey}.`))
.reduce<Record<string, string>>(
(acc, [key, val]) => ({
...acc,
[key.slice(sourceKey.length + 1)]: val,
}),
{},
)
if (Object.keys(nestedMapper).length > 0) {
return {
...result,
[targetKey]: mapData(nestedMapper, value as Record<string, unknown>, options),
}
}
}
// 处理嵌套目标路径
const targetPath = (targetKey as string).split('.')
const finalKey = targetPath.pop()!
const targetObj = targetPath.reduce<Record<string, unknown>>((obj, key) => {
if (!(key in obj)) {
obj[key] = {}
}
return obj[key] as Record<string, unknown>
}, result)
if (finalKey && targetObj) {
targetObj[finalKey] = value
}
return result
}, {})
}
return data
}
/**
* @description 生成映射表,将所有字段转换为小驼峰
* @param {Record<string, unknown>} obj - 要转换的对象
* @returns {Record<string, unknown>} 转换后的对象
*/
export const generateMapper = (obj: Record<string, unknown>) => {
return Object.entries(obj).map(([key, value]) => [
key.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()),
value,
])
}
/**
* 将对象转换为查询字符串
* @param {Record<string, any>} obj - 要转换的对象
* @returns {string} 转换后的查询字符串
*/
export const objectToQueryString = (obj: Record<string, any>) => {
return Object.entries(obj)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
}
/**
* 深度合并两个对象
* @param {Record<string, any>} target - 目标对象
* @param {Record<string, any>} source - 源对象
* @returns {Record<string, any>} 合并后的对象
*/
export const deepMerge = <T extends Record<string, any>>(target: T, source: T, isMergeArray: boolean = true): T => {
const result = { ...target }
for (const key in source) {
if (source.hasOwnProperty(key)) {
const sourceValue = source[key]
const targetValue = target[key]
if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
// 如果是数组,则合并数组
result[key] = isMergeArray ? [...targetValue, ...sourceValue] : sourceValue
} else if (isObject(sourceValue) && isObject(targetValue)) {
// 如果是对象,则递归合并
result[key] = deepMerge(targetValue, sourceValue)
} else {
// 其他情况直接覆盖
result[key] = sourceValue
}
}
}
return result
}
/**
* 判断是否为对象
* @param {any} value - 要判断的值
* @returns {boolean} 是否为对象
*/
const isObject = (value: any): boolean => {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}

View File

@@ -0,0 +1,191 @@
/**
* 文件定义:日期处理
*/
import * as R from 'ramda'
/* -------------- 1、日期处理 -------------- */
/**
* 格式化时间格式
* @param {string | number | Date} date - 日期字符串、时间戳、Date 对象
* @param {string} format - 格式化字符串
* @returns {string} 格式化后的日期字符串
*/
export const formatDate = (date: string | number | Date, format: string = 'yyyy-MM-dd HH:mm:ss'): string => {
// 处理秒级时间戳
const timestamp = !!Number(date) && date.toString().length === 10 ? new Date(Number(date) * 1000) : new Date(date)
// 使用Ramda创建日期映射
const dateMap = R.zipObj(
['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss'],
[
timestamp.getFullYear(),
timestamp.getMonth() + 1,
timestamp.getDate(),
timestamp.getHours(),
timestamp.getMinutes(),
timestamp.getSeconds(),
],
)
// 使用Ramda的reduce函数替换格式字符串中的占位符
return R.reduce(
(result: string, key: string) => {
const value = dateMap[key as keyof typeof dateMap]
// 将单位数的月、日、时、分、秒前面补0
const formattedValue = key !== 'yyyy' && value < 10 ? `0${value}` : `${value}`
// 使用正则表达式全局替换所有匹配项
return result.replace(new RegExp(key, 'g'), formattedValue)
},
format,
R.keys(dateMap),
)
}
/**
* 获取两个日期之间的天数差
* @param {string | number | Date} startDate - 开始日期
* @param {string | number | Date} endDate - 结束日期
* @returns {number} 天数差
*/
export const getDaysDiff = (startDate: string | number | Date, endDate: string | number | Date): number => {
const start = new Date(startDate)
const end = new Date(endDate)
const startDay = new Date(start.getFullYear(), start.getMonth(), start.getDate())
const endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate())
const diff = endDay.getTime() - startDay.getTime()
return Math.floor(diff / (1000 * 60 * 60 * 24))
}
/**
* 柯里化版本的getDaysDiff
* @param {string | number | Date} startDate - 开始日期
* @param {string | number | Date} endDate - 结束日期
* @returns {number} 天数差
*/
export const getDaysDiffCurried: {
(startDate: string | number | Date, endDate: string | number | Date): number
(startDate: string | number | Date): (endDate: string | number | Date) => number
} = R.curry(getDaysDiff)
/**
* 判断日期是否在指定范围内
* @param {string | number | Date} date - 要判断的日期
* @param {string | number | Date} startDate - 开始日期
* @param {string | number | Date} endDate - 结束日期
* @returns {boolean} 是否在范围内
*/
export const isDateInRange = (
date: string | number | Date,
startDate: string | number | Date,
endDate: string | number | Date,
): boolean => {
const targetTime = new Date(date).getTime()
const startTime = new Date(startDate).getTime()
const endTime = new Date(endDate).getTime()
return targetTime >= startTime && targetTime <= endTime
}
/**
* 柯里化版本的isDateInRange
* @param {string | number | Date} date - 要判断的日期
* @param {string | number | Date} startDate - 开始日期
* @param {string | number | Date} endDate - 结束日期
* @returns {boolean} 是否在范围内
*/
export const isDateInRangeCurried: {
(date: string | number | Date, startDate: string | number | Date, endDate: string | number | Date): boolean
(date: string | number | Date): {
(startDate: string | number | Date, endDate: string | number | Date): boolean
(startDate: string | number | Date): (endDate: string | number | Date) => boolean
}
(date: string | number | Date, startDate: string | number | Date): (endDate: string | number | Date) => boolean
} = R.curry(isDateInRange)
/**
* 获取指定日期的开始时间00:00:00
* @param {string | number | Date} date - 日期
* @returns {Date} 日期的开始时间
*/
export const getStartOfDay = (date: string | number | Date): Date => {
const d = new Date(date)
return new Date(d.getFullYear(), d.getMonth(), d.getDate())
}
/**
* 获取指定日期的结束时间23:59:59
* @param {string | number | Date} date - 日期
* @returns {Date} 日期的结束时间
*/
export const getEndOfDay = (date: string | number | Date): Date => {
const d = new Date(date)
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999)
}
/**
* 添加天数到指定日期
* @param {number} days - 要添加的天数
* @param {string | number | Date} date - 日期
* @returns {Date} 新日期
*/
export const addDays = (days: number, date: string | number | Date): Date => {
const result = new Date(date)
result.setDate(result.getDate() + days)
return result
}
// 柯里化版本的addDays
export const addDaysCurried: {
(days: number, date: string | number | Date): Date
(days: number): (date: string | number | Date) => Date
} = R.curry(addDays)
/**
* 格式化相对时间刚刚、x分钟前、x小时前、x天前
* @param {string | number | Date} date - 日期
* @returns {string} 格式化后的相对时间
*/
export const formatRelativeTime = (date: string | number | Date): string => {
const now = new Date().getTime()
const target = new Date(date).getTime()
const diff = now - target
if (diff < 1000 * 60) {
return '刚刚'
} else if (diff < 1000 * 60 * 60) {
return `${Math.floor(diff / (1000 * 60))}分钟前`
} else if (diff < 1000 * 60 * 60 * 24) {
return `${Math.floor(diff / (1000 * 60 * 60))}小时前`
} else if (diff < 1000 * 60 * 60 * 24 * 30) {
return `${Math.floor(diff / (1000 * 60 * 60 * 24))}天前`
} else {
return formatDate(date, 'YYYY-MM-DD')
}
}
/**
* 获取指定日期是星期几
* @param {string | number | Date} date - 日期
* @returns {string} 星期几
*/
export const getDayOfWeek = (date: string | number | Date): string => {
const days = ['日', '一', '二', '三', '四', '五', '六']
return `星期${days[new Date(date).getDay()]}`
}
/**
* 获取指定距离到期时间
* @param {string | number | Date} date - 日期
* @param {string | number | Date} expirationDate - 到期日期, 默认当前时间
* @returns {string} 距离到期时间
*/
export const getDaysUntilExpiration = (
date: string | number | Date,
expirationDate: string | number | Date = new Date(),
): string => {
const target = new Date(date)
const expiration = new Date(expirationDate)
const diff = expiration.getTime() - target.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
return days > 0 ? `${days}` : '已过期'
}

View File

@@ -0,0 +1,86 @@
/**
* 文件定义:加密解密
*/
// import JSEncrypt from 'jsencrypt'
/* -------------- 1、加密解密 -------------- */
/**
* 生成2048位RSA密钥对
* @returns {{ publicKey: string, privateKey: string }} 包含公钥和私钥的对象
*/
export const generateKeyPair = async () => {
const { JSEncrypt } = await import('jsencrypt')
const encrypt = new JSEncrypt({ default_key_size: '2048' })
encrypt.getKey()
return {
publicKey: encrypt.getPublicKey() as string,
privateKey: encrypt.getPrivateKey() as string,
}
}
/**
* RSA加密
* @param {string} str - 需要加密的字符串
* @param {string} publicKey - 公钥
* @returns {string} 加密后的字符串
*/
export const rsaEncrypt = async (str: string, publicKey: string): Promise<string> => {
const { JSEncrypt } = await import('jsencrypt')
// 基础验证
if (!str || !publicKey || publicKey.length < 10) return str
// 检查字符串长度2048位RSA密钥最大可加密245字节
const byteLength = new TextEncoder().encode(str).length
if (byteLength > 245) {
console.error('RSA加密失败: 数据长度超过245字节限制')
return str
}
try {
const encrypt = new JSEncrypt()
encrypt.setPublicKey(publicKey)
const encrypted = encrypt.encrypt(str)
// 确保加密结果有效
if (!encrypted) {
console.error('RSA加密失败')
return str
}
return encrypted
} catch (error) {
console.error('RSA加密出错:', error)
return str
}
}
/**
* RSA解密
* @param {string} str - 需要解密的字符串
* @param {string} privateKey - 私钥
* @returns {string} 解密后的字符串
*/
export const rsaDecrypt = async (str: string, privateKey: string): Promise<string> => {
const { JSEncrypt } = await import('jsencrypt')
// 基础验证
if (!str || !privateKey || privateKey.length < 10) return str
try {
const decrypt = new JSEncrypt()
decrypt.setPrivateKey(privateKey)
const decrypted = decrypt.decrypt(str)
// 确保解密结果有效
if (!decrypted) {
console.error('RSA解密失败')
return str
}
return decrypted
} catch (error) {
console.error('RSA解密出错:', error)
return str
}
}

View File

@@ -0,0 +1,99 @@
/**
* 文件定义:随机数生成
*/
import * as R from 'ramda'
/* -------------- 1、随机数生成 -------------- */
/**
* 生成指定范围内的随机整数
* @param {number} min - 最小值
* @param {number} max - 最大值
* @returns {number} 随机整数
*/
export const randomInt = (min: number, max: number): number => {
return Math.floor(Math.random() * (max - min + 1)) + min
}
/**
* 生成指定长度的随机字符串默认32位包括大小写字母和数字去除0oO1Ii
* @param {number} length - 字符串长度
* @param {object} options - 选项
* @param {boolean} options.isSpecial - 是否包含特殊字符 (默认不包含)
* @param {boolean} options.isLower - 是否包含小写字母(默认包含)
* @param {boolean} options.isUpper - 是否包含大写字母(默认包含)
* @param {boolean} options.isNumber - 是否包含数字(默认包含)
* @returns {string} 随机字符串
*/
export const randomChart = (
length: number = 32,
options: { isSpecial?: boolean; isLower?: boolean; isUpper?: boolean; isNumber?: boolean } = {},
): string => {
const { isSpecial = false, isLower = true, isUpper = true, isNumber = true } = options
let chars = ''
if (isSpecial) chars += '!@#$%^&*?'
if (isLower) chars += 'abcdefghijklmnopqrstuvwxyz'
if (isUpper) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
if (isNumber) chars += '0123456789'
const result = Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
console.log('result', result)
return result
}
/**
* 生成随机字符串,进阶版,支持包含字符最小长度,
* @param {number} length - 字符串长度
* @param {object} options - 选项
* @param {number} options.minUpper - 大写字母最小长度默认0
* @param {number} options.minLower - 小写字母最小长度默认0
* @param {number} options.minNumber - 数字最小长度默认0
* @param {number} options.minSpecial - 特殊字符最小长度默认0
*/
export const randomChartWithMinLength = (
length: number = 32,
options: { minUpper?: number; minLower?: number; minNumber?: number; minSpecial?: number } = {},
): string => {
const { minUpper = 1, minLower = 1, minNumber = 1, minSpecial = 0 } = options // 解构赋值默认值为0
let result = ''
const upperChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const lowerChars = 'abcdefghijklmnopqrstuvwxyz'
const numberChars = '0123456789'
const specialChars = '!@#$%^&*?'
// 计算已确定的最小字符数
const minTotal = minUpper + minLower + minNumber + minSpecial
if (minTotal > length) {
throw new Error('最小长度要求总和超过了指定的总长度')
}
// 生成必需的字符
const getRandomChars = (chars: string, count: number): string => {
return Array.from({ length: count }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
}
// 添加必需的字符
if (minUpper > 0) result += getRandomChars(upperChars, minUpper)
if (minLower > 0) result += getRandomChars(lowerChars, minLower)
if (minNumber > 0) result += getRandomChars(numberChars, minNumber)
if (minSpecial > 0) result += getRandomChars(specialChars, minSpecial)
// 计算剩余需要填充的长度
const remainingLength = length - minTotal
// 创建可用字符集合
let availableChars = ''
if (minUpper >= 0) availableChars += upperChars
if (minLower >= 0) availableChars += lowerChars
if (minNumber >= 0) availableChars += numberChars
if (minSpecial >= 0) availableChars += specialChars
// 填充剩余长度
result += getRandomChars(availableChars, remainingLength)
// 打乱最终结果
return result
.split('')
.sort(() => Math.random() - 0.5)
.join('')
}

View File

@@ -0,0 +1,113 @@
/**
* 文件定义:字符串处理
*/
import * as R from 'ramda'
/* -------------- 1、字符串处理 -------------- */
/**
* url字符串转换为对象
* @param {string} url - 要转换的url字符串
* @returns {Record<string, string>} 转换后的对象
*/
export const urlToObject = (url: string): Record<string, string> => {
const urlObj = new URL(url)
return Object.fromEntries(urlObj.searchParams.entries())
}
/**
* 柯里化版本的urlToObject
* @param {string} url - 要转换的url字符串
* @returns {Record<string, string>} 转换后的对象
*/
export const urlToObjectCurried: {
(url: string): Record<string, string>
(url: string): (url: string) => Record<string, string>
} = R.curry(urlToObject)
/**
* html转义支持反转义
* @param {string} str - 要转义的html字符串
* @param {boolean} isReverse - 是否反转义
* @returns {string} 转义后的html字符串
*/
export const htmlEscape = (str: string, isReverse: boolean = false): string => {
const escapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&apos;',
}
// 将escapeMap组合成正则表达式反转义将转义后的字符串转换为原始字符串
const repReg = isReverse ? R.invertObj(escapeMap) : R.map(R.identity, escapeMap)
// 将repReg组合成正则表达式
const repRegStr = Object.keys(repReg).join('|')
// 使用正则表达式替换
return str.replace(new RegExp(repRegStr, 'g'), (match: string) => {
return repReg[match as keyof typeof repReg]
})
}
/**
* 小驼峰转下划线
* @param {string} str - 要转换的驼峰字符串
* @returns {string} 转换后的下划线字符串
*/
export const camelToUnderline = (str: string): string => {
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
}
/**
* 下划线转小驼峰
* @param {string} str - 要转换的下划线字符串
* @returns {string} 转换后的驼峰字符串
*/
export const underlineToCamel = (str: string): string => {
return str.replace(/_([a-z])/g, (_, char: string) => {
return char.toUpperCase()
})
}
/**
* 下划线转大驼峰
* @param {string} str - 要转换的下划线字符串
* @returns {string} 转换后的驼峰字符串
*/
export const underlineToBigCamel = (str: string): string => {
return str.replace(/_([a-z])/g, (_, char: string) => {
return char.toUpperCase()
})
}
/**
* 大驼峰转下划线
* @param {string} str - 要转换的驼峰字符串
* @returns {string} 转换后的下划线字符串
*/
export const bigCamelToUnderline = (str: string): string => {
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
}
/**
* @description 驼峰转短横线
* @param {string} str - 要转换的驼峰字符串
* @returns {string} 转换后的短横线字符串
*/
export const kebabCase = (str: string): string => {
return bigCamelToSmallCamel(str)
.replace(/([A-Z])/g, '-$1')
.toLowerCase()
}
/**
* @description 大驼峰转小驼峰
* @param {string} str - 要转换的短横线字符串
* @returns {string} 转换后的驼峰字符串
*/
export const bigCamelToSmallCamel = (str: string): string => {
return str.replace(/^([A-Z])/, (_, char: string) => {
return char.toLowerCase()
})
}

View File

@@ -0,0 +1,108 @@
/**
* 文件定义:数据类型检查
*/
import * as R from 'ramda'
// =============== 1. 数据类型检查 ===============
/**
* 检查值是否为数字
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是数字,则返回 true否则返回 false
*/
export const isNumber = R.is(Number) as (value: unknown) => value is number
/**
* 检查值是否为字符串
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是字符串,则返回 true否则返回 false
*/
export const isString = R.is(String) as (value: unknown) => value is string
/**
* 检查值是否为对象
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是对象,则返回 true否则返回 false
*/
export const isObject = R.both(R.is(Object), R.complement(R.is(Array))) as (value: unknown) => value is object
/**
* 检查是否为布尔值
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是布尔值,则返回 true否则返回 false
*/
export const isBoolean = R.is(Boolean) as (value: unknown) => value is boolean
/**
* 检查值是否为数组
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是数组,则返回 true否则返回 false
*/
export const isArray = R.is(Array) as (value: unknown) => value is any[]
/**
* 检查是否为Porime函数
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是Porime函数则返回 true否则返回 false
*/
export const isPromise = R.is(Promise) as (value: unknown) => value is Promise<unknown>
/**
* 检查是否为函数
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是函数,则返回 true否则返回 false
*/
export const isFunction = R.is(Function) as (value: unknown) => value is Function
/**
* 检查是否为正则表达式
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是正则表达式,则返回 true否则返回 false
*/
export const isRegExp = R.is(RegExp) as (value: unknown) => value is RegExp
/**
* 检查是否为日期
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是日期,则返回 true否则返回 false
*/
export const isDate = R.is(Date) as unknown as (value: unknown) => value is Date
/**
* 检查是否为null(和undefined区分)
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是null则返回 true否则返回 false
*/
export const isNull = R.isNil as (value: unknown) => value is null
/**
* 检查是否为undefined
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是undefined则返回 true否则返回 false
*/
export const isUndefined = R.isNil as (value: unknown) => value is undefined
/**
* 检查值是否为空('', [], {},排除null和undefined
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是空,则返回 true否则返回 false
*/
export const isEmpty = R.both(R.complement(R.isNil), R.isEmpty) as (value: unknown) => value is '' | any[] | object
/* 获取值的类型
* @param {any} value - 要获取类型的值
* @returns {string} 值的类型
*/
export const getType = R.type as (value: unknown) => string
/**
* 检查值是否为指定类型
* @param {string} type - 要检查的类型
* @param {any} value - 要检查的值
* @returns {boolean} 如果值是指定类型,则返回 true否则返回 false
*/
export const isType = R.curry((type: string, value: unknown) => R.equals(getType(value), type)) as <T>(
type: string,
value: unknown,
) => value is T

View File

@@ -0,0 +1,382 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import * as browserUtils from '../src/browser'
describe('浏览器工具函数测试', () => {
beforeEach(() => {
vi.resetModules() // 清理所有模拟和存储
localStorage.clear()
sessionStorage.clear()
document.cookie = ''
})
describe('环境检测', () => {
describe('isHttps', () => {
it('应当正确判断 HTTPS 协议', () => {
const locationSpy = vi.spyOn(window, 'location', 'get')
locationSpy.mockReturnValue({ protocol: 'https:' } as Location)
expect(browserUtils.isHttps()).toBe(true)
locationSpy.mockRestore()
})
it('应当正确判断非 HTTPS 协议', () => {
const locationSpy = vi.spyOn(window, 'location', 'get')
locationSpy.mockReturnValue({ protocol: 'http:' } as Location)
expect(browserUtils.isHttps()).toBe(false)
locationSpy.mockRestore()
})
})
describe('isDev', () => {
it('应当正确判断开发环境', () => {
const originalEnv = process.env.NODE_ENV
process.env.NODE_ENV = 'development'
expect(browserUtils.isDev()).toBe(true)
process.env.NODE_ENV = originalEnv
})
it('应当正确判断非开发环境', () => {
const originalEnv = process.env.NODE_ENV
process.env.NODE_ENV = 'production'
expect(browserUtils.isDev()).toBe(false)
process.env.NODE_ENV = originalEnv
})
})
})
describe('浏览器信息获取', () => {
describe('getBrowserOSInfo', () => {
it.each([
[
'Chrome',
'Windows',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
],
['Firefox', 'macOS', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0'],
[
'Safari',
'iOS',
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1',
],
[
'Edge',
'Windows',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59',
],
['Unknown', 'Linux', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)'],
])('应当正确识别 %s 浏览器和 %s 系统', (browser, os, userAgent) => {
Object.defineProperty(navigator, 'userAgent', { value: userAgent, configurable: true })
const info = browserUtils.getBrowserOSInfo()
expect(info.browser).toBe(browser)
expect(info.os).toBe(os)
})
})
describe('getScreenInfo', () => {
it('应当返回正确的屏幕信息', () => {
Object.defineProperty(window, 'screen', {
value: { width: 1920, height: 1080 },
configurable: true,
})
Object.defineProperty(window, 'devicePixelRatio', { value: 2, configurable: true })
const screenInfo = browserUtils.getScreenInfo()
expect(screenInfo.resolution).toBe('1920x1080')
expect(screenInfo.scale).toBe(2)
})
})
})
describe('URL参数操作', () => {
describe('getUrlParam', () => {
it('应当正确获取URL参数', () => {
const locationSpy = vi.spyOn(window, 'location', 'get')
locationSpy.mockReturnValue({ search: '?name=test&age=25&empty=&special=%20%26' } as Location)
expect(browserUtils.getUrlParam('name')).toBe('test')
expect(browserUtils.getUrlParam('age')).toBe('25')
expect(browserUtils.getUrlParam('empty')).toBe('')
expect(browserUtils.getUrlParam('special')).toBe(' &')
expect(browserUtils.getUrlParam('notexist')).toBeNull()
locationSpy.mockRestore()
})
})
})
describe('存储操作', () => {
describe('Storage API', () => {
const testData = { name: 'test', value: 123, nested: { key: 'value' } }
const testKey = 'testKey'
describe('localStorage', () => {
it('应当正确设置和获取数据', () => {
browserUtils.setLocalItem(testKey, testData)
expect(browserUtils.getLocalItem(testKey)).toEqual(testData)
})
it('应当正确删除数据', () => {
browserUtils.setLocalItem(testKey, testData)
browserUtils.removeLocalItem(testKey)
expect(browserUtils.getLocalItem(testKey)).toBeNull()
})
it('应当正确清空所有数据', () => {
browserUtils.setLocalItem(testKey, testData)
browserUtils.setLocalItem('otherKey', 'value')
browserUtils.clearLocal()
expect(browserUtils.getLocalItem(testKey)).toBeNull()
expect(browserUtils.getLocalItem('otherKey')).toBeNull()
})
})
describe('sessionStorage', () => {
it('应当正确设置和获取数据', () => {
browserUtils.setSessionItem(testKey, testData)
expect(browserUtils.getSessionItem(testKey)).toEqual(testData)
})
it('应当正确删除数据', () => {
browserUtils.setSessionItem(testKey, testData)
browserUtils.removeSessionItem(testKey)
expect(browserUtils.getSessionItem(testKey)).toBeNull()
})
it('应当正确清空所有数据', () => {
browserUtils.setSessionItem(testKey, testData)
browserUtils.setSessionItem('otherKey', 'value')
browserUtils.clearSession()
expect(browserUtils.getSessionItem(testKey)).toBeNull()
expect(browserUtils.getSessionItem('otherKey')).toBeNull()
})
})
})
describe('Cookie API', () => {
const testKey = 'testKey'
const testValue = 'testValue'
beforeEach(() => {
// 清除所有 cookie
document.cookie.split(';').forEach((cookie) => {
const [key] = cookie.split('=')
document.cookie = `${key}=;expires=${new Date(0).toUTCString()};path=/`
})
})
it('应当正确设置和获取 cookie', () => {
browserUtils.setCookie(testKey, testValue)
expect(browserUtils.getCookie(testKey)).toBe(testValue)
})
it('应当正确设置带过期时间的 cookie', () => {
browserUtils.setCookie(testKey, testValue, 1)
expect(browserUtils.getCookie(testKey)).toBe(testValue)
})
it('应当正确处理特殊字符', () => {
const specialValue = 'test value with spaces & special chars'
browserUtils.setCookie(testKey, specialValue)
expect(browserUtils.getCookie(testKey)).toBe(specialValue)
})
it('应当正确删除 cookie', () => {
browserUtils.setCookie(testKey, testValue)
browserUtils.deleteCookie(testKey)
expect(browserUtils.getCookie(testKey)).toBeNull()
})
it('应当正确清空所有 cookie', () => {
browserUtils.setCookie(testKey, testValue)
browserUtils.setCookie('otherKey', 'otherValue')
browserUtils.clearCookie()
expect(browserUtils.getCookie(testKey)).toBeNull()
expect(browserUtils.getCookie('otherKey')).toBeNull()
})
it('应当正确处理 HTTPS 前缀', () => {
const locationSpy = vi.spyOn(window, 'location', 'get')
locationSpy.mockReturnValue({ protocol: 'https:' } as Location)
browserUtils.setCookie(testKey, testValue)
expect(browserUtils.getCookie(testKey)).toBe(testValue)
expect(document.cookie).toContain('https_')
locationSpy.mockRestore()
})
})
})
describe('柯里化函数', () => {
it('应当正确使用柯里化版本的 getUrlParam', () => {
const locationSpy = vi.spyOn(window, 'location', 'get')
locationSpy.mockReturnValue({ search: '?name=test&age=25' } as Location)
expect(browserUtils.getUrlParamCurried('name')).toBe('test')
expect(browserUtils.getUrlParamCurried('age')).toBe('25')
locationSpy.mockRestore()
})
it('应当正确使用柯里化版本的 setCookie', () => {
const setCookieForKey = browserUtils.setCookieCurried('testKey')
setCookieForKey('testValue', 1)
expect(browserUtils.getCookie('testKey')).toBe('testValue')
})
it('应当正确使用柯里化版本的 getCookie', () => {
browserUtils.setCookie('testKey', 'testValue')
expect(browserUtils.getCookieCurried('testKey')).toBe('testValue')
})
it('应当正确使用柯里化版本的 setStorageItem', () => {
const setItemForKey = browserUtils.setStorageItemCurried('testKey')
const testData = { test: 'value' }
setItemForKey(testData, localStorage)
expect(JSON.parse(localStorage.getItem('testKey') || '')).toEqual(testData)
})
it('应当正确使用柯里化版本的 getStorageItem', () => {
const testData = { test: 'value' }
localStorage.setItem('testKey', JSON.stringify(testData))
expect(browserUtils.getStorageItemCurried('testKey')(localStorage)).toEqual(testData)
})
})
describe('IndexedDB', () => {
let dbManager: browserUtils.IndexedDBManager
const testConfig: browserUtils.IndexedDBConfig = {
dbName: 'testDB',
version: 1,
stores: {
users: {
keyPath: 'id',
indexes: [
{ name: 'name', keyPath: 'name' },
{ name: 'email', keyPath: 'email', options: { unique: true } },
],
},
},
}
beforeEach(async () => {
// 确保在创建新的数据库管理器之前删除旧的数据库
await new Promise<void>((resolve) => {
const deleteRequest = indexedDB.deleteDatabase(testConfig.dbName)
deleteRequest.onsuccess = () => resolve()
deleteRequest.onerror = () => resolve() // 即使出错也继续
deleteRequest.onblocked = () => resolve() // 处理阻塞情况
})
// 创建新的数据库管理器实例
dbManager = new browserUtils.IndexedDBManager(testConfig)
// 等待数据库连接和初始化完成
await dbManager.connect()
})
afterEach(async () => {
// 关闭数据库连接
if (dbManager) {
dbManager.close()
}
// 删除测试数据库
await new Promise<void>((resolve) => {
const deleteRequest = indexedDB.deleteDatabase(testConfig.dbName)
deleteRequest.onsuccess = () => resolve()
deleteRequest.onerror = () => resolve() // 即使出错也继续
deleteRequest.onblocked = () => {
// 等待连接关闭后继续
setTimeout(resolve, 100)
}
})
})
it('应当正确连接数据库', async () => {
const db = await dbManager.connect()
expect(db).toBeDefined()
expect(db.name).toBe(testConfig.dbName)
expect(db.version).toBe(testConfig.version)
expect(Array.from(db.objectStoreNames)).toContain('users')
})
it('应当正确添加和获取数据', async () => {
const testUser = { id: 1, name: 'Test User', email: 'test@example.com' }
// 添加数据
await dbManager.add('users', testUser)
// 获取数据
const result = await dbManager.get('users', 1)
expect(result).toEqual(testUser)
})
it('应当正确更新数据', async () => {
const testUser = { id: 1, name: 'Test User', email: 'test@example.com' }
await dbManager.add('users', testUser)
const updatedUser = { ...testUser, name: 'Updated User' }
await dbManager.put('users', updatedUser)
const result = await dbManager.get('users', 1)
expect(result).toEqual(updatedUser)
})
it('应当正确删除数据', async () => {
const testUser = { id: 1, name: 'Test User', email: 'test@example.com' }
await dbManager.add('users', testUser)
await dbManager.delete('users', 1)
const result = await dbManager.get('users', 1)
expect(result).toBeUndefined()
})
it('应当正确通过索引查询数据', async () => {
const testUser = { id: 1, name: 'Test User', email: 'test@example.com' }
await dbManager.add('users', testUser)
const result = await dbManager.getByIndex('users', 'email', 'test@example.com')
expect(result).toEqual(testUser)
})
it('应当正确获取所有数据', async () => {
const users = [
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' },
]
await dbManager.addBatch('users', users)
const results = await dbManager.getAll('users')
expect(results).toHaveLength(2)
expect(results).toEqual(expect.arrayContaining(users))
})
it('应当正确遍历数据', async () => {
const users = [
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' },
]
await dbManager.addBatch('users', users)
const results: typeof users = []
await dbManager.forEach<(typeof users)[0]>('users', (item) => {
results.push(item)
})
expect(results).toHaveLength(2)
expect(results).toEqual(expect.arrayContaining(users))
})
it('应当正确清空数据', async () => {
const users = [
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' },
]
await dbManager.addBatch('users', users)
await dbManager.clear('users')
const results = await dbManager.getAll('users')
expect(results).toHaveLength(0)
})
})
})

View File

@@ -0,0 +1,169 @@
import { describe, it, expect } from 'vitest'
import * as businessUtils from '../src/business'
describe('业务工具函数测试', () => {
describe('正则验证测试', () => {
describe('邮箱验证', () => {
it.each([
['valid@email.com', true],
['invalid.email', false],
['test@test.cn', true],
['@invalid.com', false],
['test@.com', false],
])('应当正确验证邮箱 %s', (email, expected) => {
expect(businessUtils.isEmail(email)).toBe(expected)
})
})
describe('手机号验证', () => {
it.each([
['13812345678', true],
['12345678901', false],
['19912345678', true],
['1381234567', false],
['138123456789', false],
])('应当正确验证手机号 %s', (phone, expected) => {
expect(businessUtils.isPhone(phone)).toBe(expected)
})
})
describe('身份证号验证', () => {
it.each([
['440101199001011234', true],
['44010119900101123X', true],
['440101199001011', false],
['44010119900101123Y', false],
])('应当正确验证身份证号 %s', (idCard, expected) => {
expect(businessUtils.isIdCard(idCard)).toBe(expected)
})
})
describe('URL验证', () => {
it.each([
['https://www.example.com', true],
['http://localhost:3000', true],
['ftp://files.example.com', true],
['invalid-url', false],
])('应当正确验证URL %s', (url, expected) => {
expect(businessUtils.isUrl(url)).toBe(expected)
})
})
describe('IP地址验证', () => {
describe('IPv4验证', () => {
it.each([
['192.168.1.1', true],
['256.1.2.3', false],
['1.2.3.4', true],
['192.168.001.1', false],
])('应当正确验证IPv4地址 %s', (ip, expected) => {
expect(businessUtils.isIpv4(ip)).toBe(expected)
})
})
describe('IPv6验证', () => {
it.each([
['2001:0db8:85a3:0000:0000:8a2e:0370:7334', true],
['fe80::1', true],
['::1', true],
['2001::7334', true],
['invalid-ipv6', false],
])('应当正确验证IPv6地址 %s', (ip, expected) => {
expect(businessUtils.isIpv6(ip)).toBe(expected)
})
})
})
describe('MAC地址验证', () => {
it.each([
['00-B0-D0-63-C2-26', true],
['00-b0-d0-63-c2-26', true],
['00:B0:D0:63:C2:26', false],
['00-B0-D0-63-C2', false],
])('应当正确验证MAC地址 %s', (mac, expected) => {
expect(businessUtils.isMac(mac)).toBe(expected)
})
})
describe('中文验证', () => {
it.each([
['中文', true],
['中文123', false],
['Chinese', false],
['中文!', false],
])('应当正确验证中文 %s', (str, expected) => {
expect(businessUtils.isChinese(str)).toBe(expected)
})
})
})
describe('业务操作测试', () => {
describe('手机号加密', () => {
it('应当正确加密手机号', () => {
expect(businessUtils.encryptPhone('13812345678')).toBe('138****5678')
})
})
describe('身份证号加密', () => {
it('应当正确加密身份证号', () => {
expect(businessUtils.encryptIdCard('440101199001011234')).toBe('440101****1234')
expect(businessUtils.encryptIdCard('44010119900101123X')).toBe('440101****123X')
})
})
describe('版本号比较', () => {
it.each([
['1.0.0', '1.0.1', -1],
['1.0.1', '1.0.0', 1],
['1.0.0', '1.0.0', 0],
['1.0', '1.0.0', 0],
['1.0.0', '1', 0],
['1.1', '1.0.1', 1],
])('比较版本号 %s 和 %s 应当返回 %i', (v1, v2, expected) => {
expect(businessUtils.compareVersion(v1, v2)).toBe(expected)
})
})
describe('字节转换', () => {
it.each([
[0, 2, true, '', '0 B'],
[1024, 0, true, '', '1 KB'],
[1024 * 1024, 2, true, '', '1.00 MB'],
[1024 * 1024 * 1024, 0, true, '', '1 GB'],
[1500, 2, true, 'KB', '1.46 KB'],
[1500, 2, false, 'KB', '1.46'],
])('转换 %i 字节应当返回 %s', (bytes, fixed, isUnit, endUnit, expected) => {
expect(businessUtils.formatBytes(bytes, fixed, isUnit, endUnit)).toBe(expected)
})
})
describe('分页字符串转换', () => {
it.each([
["class='Pcount'>共100条<", 100],
["class='Pcount'>共0条<", 0],
['invalid string', 0],
])('应当正确转换分页字符串 %s', (page, expected) => {
expect(businessUtils.formatPage(page)).toBe(expected)
})
})
})
describe('代理配置测试', () => {
it('应当正确生成params格式的代理配置', () => {
const config = businessUtils.getProxyConfig('test-key', 'params') as {
request_time: number
request_token: string
}
expect(config).toHaveProperty('request_time')
expect(config).toHaveProperty('request_token')
expect(typeof config.request_time).toBe('number')
expect(typeof config.request_token).toBe('string')
})
it('应当正确生成query格式的代理配置', () => {
const config = businessUtils.getProxyConfig('test-key', 'query')
expect(typeof config).toBe('string')
expect(config).toMatch(/request_time=\d+&request_token=[a-f0-9]+/)
})
})
})

View File

@@ -0,0 +1,121 @@
import { describe, it, expect } from 'vitest'
import * as dataUtils from '../src/data'
describe('数据处理工具函数测试', () => {
describe('数据转换', () => {
describe('objectToString', () => {
it('应当正确将对象值转换为字符串', () => {
const input = { a: 1, b: true, c: null }
const expected = { a: '1', b: 'true', c: 'null' }
expect(dataUtils.objectToString(input)).toEqual(expected)
})
})
describe('arrayToObject', () => {
it('应当正确将数组转换为对象', () => {
const input = [
{ id: '1', name: 'test1' },
{ id: '2', name: 'test2' },
]
const expected = {
'1': { id: '1', name: 'test1' },
'2': { id: '2', name: 'test2' },
}
expect(dataUtils.arrayToObject('id', input)).toEqual(expected)
})
})
describe('flattenObject', () => {
it('应当正确扁平化对象', () => {
const input = {
a: 1,
b: {
c: 2,
d: {
e: 3,
},
},
}
const expected = {
a: 1,
'b.c': 2,
'b.d.e': 3,
}
console.log('dataUtils.flattenObject(input)', dataUtils.flattenObject(input))
expect(dataUtils.flattenObject(input)).toEqual(expected)
})
})
})
describe('数据验证', () => {
describe('matchesPattern', () => {
it('应当正确验证正则表达式', () => {
const pattern = /^test\d+$/
expect(dataUtils.matchesPattern(pattern, 'test123')).toBe(true)
expect(dataUtils.matchesPattern(pattern, 'test')).toBe(false)
})
})
describe('hasRequiredKeys', () => {
it('应当正确验证必需的键', () => {
const obj = { a: 1, b: 2, c: 3 }
expect(dataUtils.hasRequiredKeys(obj, ['a', 'b'])).toBe(true)
expect(dataUtils.hasRequiredKeys(obj, ['a', 'd'])).toBe(false)
})
})
describe('isInRange', () => {
it('应当正确验证值是否在范围内', () => {
expect(dataUtils.isInRange(1, 10, 5)).toBe(true)
expect(dataUtils.isInRange(1, 10, 0)).toBe(false)
expect(dataUtils.isInRange(1, 10, 11)).toBe(false)
})
})
})
describe('数据过滤与重组', () => {
describe('filterObject', () => {
it('应当正确过滤对象的属性', () => {
const input = { a: 1, b: null, c: undefined, d: 'test' }
const expected = { a: 1, d: 'test' }
expect(dataUtils.filterObject((value) => value != null, input)).toEqual(expected)
})
})
describe('groupByKey', () => {
it('应当正确按键对数组进行分组', () => {
const input = [
{ type: 'A', value: 1 },
{ type: 'B', value: 2 },
{ type: 'A', value: 3 },
]
const expected = {
A: [
{ type: 'A', value: 1 },
{ type: 'A', value: 3 },
],
B: [{ type: 'B', value: 2 }],
}
expect(dataUtils.groupByKey('type', input)).toEqual(expected)
})
})
describe('pluckDeep', () => {
it('应当正确提取深层属性', () => {
const input = [{ a: { b: { c: 1 } } }, { a: { b: { c: 2 } } }]
expect(dataUtils.pluckDeep(['a', 'b', 'c'], input)).toEqual([1, 2])
})
})
describe('flattenAndUniq', () => {
it('应当正确扁平化和去重数组', () => {
const input = [
[1, 2],
[2, 3],
[3, 4],
]
expect(dataUtils.flattenAndUniq(input)).toEqual([1, 2, 3, 4])
})
})
})
})

View File

@@ -0,0 +1,123 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import * as dateUtils from '../src/date'
describe('日期处理工具函数测试', () => {
describe('formatDate', () => {
it('应当正确格式化日期字符串', () => {
const date = new Date('2024-02-27 14:30:45')
expect(dateUtils.formatDate(date)).toBe('2024-02-27 14:30:45')
expect(dateUtils.formatDate(date, 'YYYY-MM-DD')).toBe('2024-02-27')
expect(dateUtils.formatDate(date, 'HH:mm:ss')).toBe('14:30:45')
})
it('应当正确处理单位数的月日时分秒', () => {
const date = new Date('2024-01-05 09:05:08')
expect(dateUtils.formatDate(date)).toBe('2024-01-05 09:05:08')
})
})
describe('getDaysDiff', () => {
it('应当正确计算两个日期之间的天数差', () => {
const start = new Date('2024-02-27')
const end = new Date('2024-03-01')
expect(dateUtils.getDaysDiff(start, end)).toBe(3)
})
it('应当正确处理同一天的情况', () => {
const date = new Date('2024-02-27')
expect(dateUtils.getDaysDiff(date, date)).toBe(0)
})
})
describe('isDateInRange', () => {
it('应当正确判断日期是否在范围内', () => {
const start = new Date('2024-02-01')
const end = new Date('2024-02-29')
const date = new Date('2024-02-15')
expect(dateUtils.isDateInRange(date, start, end)).toBe(true)
})
it('应当正确处理边界情况', () => {
const start = new Date('2024-02-01')
const end = new Date('2024-02-29')
expect(dateUtils.isDateInRange(start, start, end)).toBe(true)
expect(dateUtils.isDateInRange(end, start, end)).toBe(true)
})
it('应当正确处理范围外的情况', () => {
const start = new Date('2024-02-01')
const end = new Date('2024-02-29')
const before = new Date('2024-01-31')
const after = new Date('2024-03-01')
expect(dateUtils.isDateInRange(before, start, end)).toBe(false)
expect(dateUtils.isDateInRange(after, start, end)).toBe(false)
})
})
describe('getStartOfDay和getEndOfDay', () => {
it('应当正确获取一天的开始时间', () => {
const date = new Date('2024-02-27 14:30:45')
const start = dateUtils.getStartOfDay(date)
expect(start.getHours()).toBe(0)
expect(start.getMinutes()).toBe(0)
expect(start.getSeconds()).toBe(0)
})
it('应当正确获取一天的结束时间', () => {
const date = new Date('2024-02-27 14:30:45')
const end = dateUtils.getEndOfDay(date)
expect(end.getHours()).toBe(23)
expect(end.getMinutes()).toBe(59)
expect(end.getSeconds()).toBe(59)
})
})
describe('addDays', () => {
it('应当正确添加天数', () => {
const date = new Date('2024-02-27')
expect(dateUtils.addDays(1, date).toDateString()).toBe(new Date('2024-02-28').toDateString())
expect(dateUtils.addDays(-1, date).toDateString()).toBe(new Date('2024-02-26').toDateString())
})
})
describe('formatRelativeTime', () => {
beforeEach(() => {
// 固定当前时间为2024-02-27 14:30:00
vi.useFakeTimers()
vi.setSystemTime(new Date('2024-02-27 14:30:00'))
})
afterEach(() => {
vi.useRealTimers()
})
it('应当正确格式化相对时间', () => {
expect(dateUtils.formatRelativeTime(new Date('2024-02-27 14:29:30'))).toBe('刚刚')
expect(dateUtils.formatRelativeTime(new Date('2024-02-27 14:25:00'))).toBe('5分钟前')
expect(dateUtils.formatRelativeTime(new Date('2024-02-27 13:30:00'))).toBe('1小时前')
expect(dateUtils.formatRelativeTime(new Date('2024-02-26 14:30:00'))).toBe('1天前')
expect(dateUtils.formatRelativeTime(new Date('2024-01-27 14:30:00'))).toBe('2024-01-27')
})
})
describe('getDayOfWeek', () => {
it('应当正确获取星期几', () => {
expect(dateUtils.getDayOfWeek(new Date('2024-02-27'))).toBe('星期二')
expect(dateUtils.getDayOfWeek(new Date('2024-02-25'))).toBe('星期日')
})
})
describe('getDaysUntilExpiration', () => {
it('应当正确计算到期天数', () => {
const current = new Date('2024-02-27')
const future = new Date('2024-03-01')
expect(dateUtils.getDaysUntilExpiration(current, future)).toBe('3天')
})
it('应当正确处理已过期情况', () => {
const current = new Date('2024-02-27')
const past = new Date('2024-02-26')
expect(dateUtils.getDaysUntilExpiration(current, past)).toBe('已过期')
})
})
})

View File

@@ -0,0 +1,85 @@
import { describe, it, expect } from 'vitest'
import * as encipherUtils from '../src/encipher'
describe('加密解密工具函数测试', () => {
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArWtsSxxqzT8X9D3yVF12
6WHBd+6WZw1TSoatATB6djpe05xwPKOFrNSbOz/tqm6zOhv47w8roO8p978XmHiv
fOuYZxAoCCJUZBG5BxMgEcO5uwue/ll1Hp5VaxvI52Vnuoh9HLx8LpxB0FPXvAjm
cJ7pvgs8Tnox8o2idWN25D1HTeITME+9wBcs7aubNFoUczFDk5+q33mW+i31C30r
DK9/j0odoy0NYGA5DxQiOWpqK3ljaO+40XWYqbWBfq+9LeTPMKT8UARxiSTXumKL
R5p35l0B1CoqpedhszPFvfHzpIPHSzk+uDAwMdR7EprrGinYzOTiTs/wy/ggOICe
uwIDAQAB
-----END PUBLIC KEY-----`
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCta2xLHGrNPxf0
PfJUXXbpYcF37pZnDVNKhq0BMHp2Ol7TnHA8o4Ws1Js7P+2qbrM6G/jvDyug7yn3
vxeYeK9865hnECgIIlRkEbkHEyARw7m7C57+WXUenlVrG8jnZWe6iH0cvHwunEHQ
U9e8COZwnum+CzxOejHyjaJ1Y3bkPUdN4hMwT73AFyztq5s0WhRzMUOTn6rfeZb6
LfULfSsMr3+PSh2jLQ1gYDkPFCI5amoreWNo77jRdZiptYF+r70t5M8wpPxQBHGJ
JNe6YotHmnfmXQHUKiql52GzM8W98fOkg8dLOT64MDAx1HsSmusaKdjM5OJOz/DL
+CA4gJ67AgMBAAECggEAPS0LC8gfiP375kZACTDbdOLuS++XkQzrV/wAZc4DNVfM
AdxK36lTy69If3NC1P+uLA6YF0UDwAb+iA4aNchFJ804ewsBBDWQDakO24cMphek
mm40DUfjgASc32byzWZBXFUvxYZcTFkFAofBL+z31bzJeigegxSqMAV0zPJki4jZ
pWLiVYiIQ+SSG75mr/c9VJdTV7/kekNpkaXmTaVRSfKqROQpV7niknWnuNYSUGUF
apY7JbEJiIOdB6Tc6aGIXzAAlr3klCF22cTTBbBrP3kDGm2Bmr2Hqrxe5I0Eo05j
9Su+TiH26tBG4/FoMm6l3nT8O2fjntv1eXUuvluLOQKBgQDcB4oCP4f/bIysns8u
dGsdsvoZ/e57QVHcRn5G8A7KYma5uJ96Ll0eWgSLoH3wZ2YuRbqrQX7Y3dvfAd6I
LvB6lOJ6bpDc5bn3wvGXb6qF/9h/m2HIyepE9B7m5omXCZ4tFiThbUX1m/TrhzW8
IWKe8qY25FuIji9thzrFbCdGTQKBgQDJxTbWuXMAv9lojXwu73XPMDlgt5LG0eEK
S4QYxMm7VEHtXQ54q8ExKTETuzcTHADqBBEx7/Zhlv6Bxxbe7ghjHI3Mv+F7T5qq
5zZ8n62c0UWttd0XqbC3jLtiX3wMtM2WnUGdgWA4/YBbWlj7x6cuO8ptL027hR/k
/ta1vz8NJwKBgCBFSsyBnOStewRmVmSt1ngIo/3j7HJPZj40aJjm5IRyYjajCWDW
I/orobcI1u/HeokW2QX9GSmdgH34vDalC8guxfjG9qAvYVMhWGWpjw0QNSSiGXll
g+KRG2cqMMviMTzTnp0hdb1MHmPc9Nie3OQLGq26WGJy2CnsR4ZlEm2RAoGBALof
0Xl8MskDMKNQuLh3Lp7EZnmAfcYn/0bG6IEMrua+T96NE/dewOT/kYUZEzHuiC1X
OSFusUHOztGafM+ClnwO8ANrEa31fcCfbtTBW56oMXWPqPbWEu0OxiB14nG6K1f/
knKf0MphlpEuo50GzIJKp23W0AbmQ8izCA857wjLAoGAYqCMYuQuLWu/0WNyRR7z
ia3CeKh2L8Y/0IhXAqwMFwxZgbHKc9Bw2Q/Vqj45ERhMQntDDzVeUhZZVXVnmWsj
58YF7VfNOok4B0UU4uOGus6XYsAD4mm9gTiaFijPIFY1icd9p8wl7MmVWBlvtqmE
SQK1rJJCb92DHknrKmUUj0o=
-----END RSA PRIVATE KEY-----
`
describe('rsaEncrypt', () => {
it('应当正确加密字符串', () => {
const text = 'Hello World'
const encrypted = encipherUtils.rsaEncrypt(text, publicKey)
expect(encrypted).toBeTruthy()
expect(encrypted).not.toBe(text)
})
it('当公钥无效时应当返回原文', () => {
const text = 'Hello World'
expect(encipherUtils.rsaEncrypt(text, '')).toBe(text)
})
})
describe('rsaDecrypt', () => {
it('应当正确解密字符串', () => {
const text = 'Hello World'
const encrypted = encipherUtils.rsaEncrypt(text, publicKey)
const decrypted = encipherUtils.rsaDecrypt(encrypted, privateKey)
expect(decrypted).toBe(text)
})
it('当私钥无效时应当返回原文', () => {
const text = 'Hello World'
expect(encipherUtils.rsaDecrypt(text, '')).toBe(text)
})
})
describe('加密解密集成测试', () => {
it('应当能够正确完成加密解密循环', () => {
const testCases = ['Hello World', '123456', 'Special @#$% Characters', '中文测试']
testCases.forEach((text) => {
const encrypted = encipherUtils.rsaEncrypt(text, publicKey)
const decrypted = encipherUtils.rsaDecrypt(encrypted, privateKey)
expect(decrypted).toBe(text)
})
})
})
})

View File

@@ -0,0 +1,106 @@
import { describe, it, expect } from 'vitest'
import * as randomUtils from '../src/random'
describe('随机数生成工具函数测试', () => {
describe('randomInt', () => {
it('应当生成指定范围内的随机整数', () => {
const min = 1
const max = 10
for (let i = 0; i < 100; i++) {
const result = randomUtils.randomInt(min, max)
expect(result).toBeGreaterThanOrEqual(min)
expect(result).toBeLessThanOrEqual(max)
expect(Number.isInteger(result)).toBe(true)
}
})
it('应当正确处理最小值等于最大值的情况', () => {
const value = 5
expect(randomUtils.randomInt(value, value)).toBe(value)
})
})
describe('randomChart', () => {
it('应当生成指定长度的随机字符串', () => {
const length = 32
const result = randomUtils.randomChart(length)
expect(result.length).toBe(length)
})
it('应当正确处理不同选项', () => {
// 只包含特殊字符
const specialOnly = randomUtils.randomChart(10, {
isSpecial: true,
isLower: false,
isUpper: false,
isNumber: false,
})
expect(specialOnly).toMatch(/^[!@#$%^&*?]+$/)
// 只包含小写字母
const lowerOnly = randomUtils.randomChart(10, {
isSpecial: false,
isLower: true,
isUpper: false,
isNumber: false,
})
expect(lowerOnly).toMatch(/^[a-z]+$/)
// 只包含大写字母
const upperOnly = randomUtils.randomChart(10, {
isSpecial: false,
isLower: false,
isUpper: true,
isNumber: false,
})
expect(upperOnly).toMatch(/^[A-Z]+$/)
// 只包含数字
const numberOnly = randomUtils.randomChart(10, {
isSpecial: false,
isLower: false,
isUpper: false,
isNumber: true,
})
expect(numberOnly).toMatch(/^[0-9]+$/)
})
})
describe('randomChartWithMinLength', () => {
it('应当生成包含最小长度要求的随机字符串', () => {
const length = 32
const options = {
minUpper: 5,
minLower: 5,
minNumber: 5,
minSpecial: 2,
}
const result = randomUtils.randomChartWithMinLength(length, options)
expect(result.length).toBe(length)
expect(result.match(/[A-Z]/g)?.length).toBeGreaterThanOrEqual(options.minUpper)
expect(result.match(/[a-z]/g)?.length).toBeGreaterThanOrEqual(options.minLower)
expect(result.match(/[0-9]/g)?.length).toBeGreaterThanOrEqual(options.minNumber)
expect(result.match(/[!@#$%^&*?]/g)?.length).toBeGreaterThanOrEqual(options.minSpecial)
})
it('当最小长度要求总和超过指定长度时应当抛出错误', () => {
const length = 10
const options = {
minUpper: 4,
minLower: 4,
minNumber: 4,
minSpecial: 4,
}
expect(() => randomUtils.randomChartWithMinLength(length, options)).toThrow()
})
it('应当生成不同的随机字符串', () => {
const length = 32
const options = { minUpper: 1, minLower: 1, minNumber: 1, minSpecial: 0 }
const result1 = randomUtils.randomChartWithMinLength(length, options)
const result2 = randomUtils.randomChartWithMinLength(length, options)
expect(result1).not.toBe(result2)
})
})
})

View File

@@ -0,0 +1,75 @@
import { describe, it, expect } from 'vitest'
import * as stringUtils from '../src/string'
describe('字符串处理工具函数测试', () => {
describe('urlToObject', () => {
it('应当正确解析URL参数', () => {
const url = 'https://example.com/path?name=test&age=25&type=user'
const result = stringUtils.urlToObject(url)
expect(result).toEqual({
name: 'test',
age: '25',
type: 'user',
})
})
it('应当正确处理空参数', () => {
const url = 'https://example.com/path'
const result = stringUtils.urlToObject(url)
expect(result).toEqual({})
})
it('应当正确处理特殊字符', () => {
const url = 'https://example.com/path?name=test%20name&email=test%40example.com'
const result = stringUtils.urlToObject(url)
expect(result).toEqual({
name: 'test name',
email: 'test@example.com',
})
})
})
describe('htmlEscape', () => {
it('应当正确转义HTML字符', () => {
const html = '<div class="test">Hello & World</div>'
const escaped = stringUtils.htmlEscape(html)
expect(escaped).toBe('&lt;div class=&quot;test&quot;&gt;Hello &amp; World&lt;/div&gt;')
})
it('应当正确反转义HTML字符', () => {
const escaped = '&lt;div class=&quot;test&quot;&gt;Hello &amp; World&lt;/div&gt;'
const unescaped = stringUtils.htmlEscape(escaped, true)
expect(unescaped).toBe('<div class="test">Hello & World</div>')
})
})
describe('驼峰和下划线转换', () => {
describe('camelToUnderline', () => {
it('应当正确将小驼峰转换为下划线', () => {
expect(stringUtils.camelToUnderline('userName')).toBe('user_name')
expect(stringUtils.camelToUnderline('userFirstName')).toBe('user_first_name')
})
})
describe('underlineToCamel', () => {
it('应当正确将下划线转换为小驼峰', () => {
expect(stringUtils.underlineToCamel('user_name')).toBe('userName')
expect(stringUtils.underlineToCamel('user_first_name')).toBe('userFirstName')
})
})
describe('underlineToBigCamel', () => {
it('应当正确将下划线转换为大驼峰', () => {
expect(stringUtils.underlineToBigCamel('user_name')).toBe('userName')
expect(stringUtils.underlineToBigCamel('user_first_name')).toBe('userFirstName')
})
})
describe('bigCamelToUnderline', () => {
it('应当正确将大驼峰转换为下划线', () => {
expect(stringUtils.bigCamelToUnderline('UserName')).toBe('_user_name')
expect(stringUtils.bigCamelToUnderline('UserFirstName')).toBe('_user_first_name')
})
})
})
})

View File

@@ -0,0 +1,102 @@
import { describe, it, expect } from 'vitest'
import * as typeUtils from '../src/type'
describe('类型检查工具函数测试', () => {
describe('基础类型检查', () => {
it('应当正确检查数字类型', () => {
expect(typeUtils.isNumber(123)).toBe(true)
expect(typeUtils.isNumber('123')).toBe(false)
expect(typeUtils.isNumber(NaN)).toBe(true)
expect(typeUtils.isNumber(Infinity)).toBe(true)
})
it('应当正确检查字符串类型', () => {
expect(typeUtils.isString('test')).toBe(true)
expect(typeUtils.isString(123)).toBe(false)
expect(typeUtils.isString('')).toBe(true)
})
it('应当正确检查对象类型', () => {
expect(typeUtils.isObject({})).toBe(true)
expect(typeUtils.isObject([])).toBe(false)
expect(typeUtils.isObject(null)).toBe(false)
})
it('应当正确检查布尔类型', () => {
expect(typeUtils.isBoolean(true)).toBe(true)
expect(typeUtils.isBoolean(false)).toBe(true)
expect(typeUtils.isBoolean(1)).toBe(false)
})
it('应当正确检查数组类型', () => {
expect(typeUtils.isArray([])).toBe(true)
expect(typeUtils.isArray([1, 2, 3])).toBe(true)
expect(typeUtils.isArray({})).toBe(false)
})
})
describe('特殊类型检查', () => {
it('应当正确检查Promise类型', () => {
expect(typeUtils.isPromise(Promise.resolve())).toBe(true)
expect(typeUtils.isPromise({})).toBe(false)
})
it('应当正确检查函数类型', () => {
expect(typeUtils.isFunction(() => {})).toBe(true)
expect(typeUtils.isFunction(function () {})).toBe(true)
expect(typeUtils.isFunction({})).toBe(false)
})
it('应当正确检查正则表达式类型', () => {
expect(typeUtils.isRegExp(/test/)).toBe(true)
expect(typeUtils.isRegExp(new RegExp('test'))).toBe(true)
expect(typeUtils.isRegExp({})).toBe(false)
})
it('应当正确检查日期类型', () => {
expect(typeUtils.isDate(new Date())).toBe(true)
expect(typeUtils.isDate('2024-02-27')).toBe(false)
})
})
describe('空值检查', () => {
it('应当正确检查null值', () => {
expect(typeUtils.isNull(null)).toBe(true)
expect(typeUtils.isNull(undefined)).toBe(true)
expect(typeUtils.isNull(0)).toBe(false)
})
it('应当正确检查undefined值', () => {
expect(typeUtils.isUndefined(undefined)).toBe(true)
expect(typeUtils.isUndefined(null)).toBe(true)
expect(typeUtils.isUndefined(0)).toBe(false)
})
it('应当正确检查空值', () => {
expect(typeUtils.isEmpty('')).toBe(true)
expect(typeUtils.isEmpty([])).toBe(true)
expect(typeUtils.isEmpty({})).toBe(true)
expect(typeUtils.isEmpty('test')).toBe(false)
expect(typeUtils.isEmpty([1])).toBe(false)
expect(typeUtils.isEmpty({ key: 'value' })).toBe(false)
})
})
describe('类型获取和比较', () => {
it('应当正确获取值的类型', () => {
expect(typeUtils.getType(123)).toBe('Number')
expect(typeUtils.getType('test')).toBe('String')
expect(typeUtils.getType(true)).toBe('Boolean')
expect(typeUtils.getType([])).toBe('Array')
expect(typeUtils.getType({})).toBe('Object')
})
it('应当正确检查指定类型', () => {
expect(typeUtils.isType('Number', 123)).toBe(true)
expect(typeUtils.isType('String', 'test')).toBe(true)
expect(typeUtils.isType('Boolean', true)).toBe(true)
expect(typeUtils.isType('Array', [])).toBe(true)
expect(typeUtils.isType('Object', {})).toBe(true)
})
})
})

View File

@@ -0,0 +1,14 @@
{
"extends": "@baota/typescript/base.json",
"compilerOptions": {
"outDir": "dist", // 输出目录
"rootDir": "src",
"baseUrl": "./",
"sourceMap": true,
"module": "ESNext",
"moduleResolution": "Node",
"target": "ESNext"
},
"include": ["src/*.ts", "src/*.tsx", "src/*.d.ts", "*.d.ts"],
"exclude": ["node_modules", "dist"]
}

0
frontend/packages/utils/types.d.ts vendored Normal file
View File

View File

@@ -0,0 +1,45 @@
import { defineConfig } from 'vite'
import { resolve } from 'path'
import dts from 'vite-plugin-dts'
export default defineConfig({
root: resolve(__dirname, './src'),
plugins: [
dts({
include: ['*.ts'],
beforeWriteFile: (filePath, content) => ({
filePath: filePath.replace(/src/, ''),
content,
}),
}),
],
build: {
outDir: resolve(__dirname, 'dist'),
emptyOutDir: true,
lib: {
entry: {
browser: resolve(__dirname, 'src/browser.ts'),
business: resolve(__dirname, 'src/business.ts'),
data: resolve(__dirname, 'src/data.ts'),
date: resolve(__dirname, 'src/date.ts'),
encipher: resolve(__dirname, 'src/encipher.ts'),
random: resolve(__dirname, 'src/random.ts'),
string: resolve(__dirname, 'src/string.ts'),
type: resolve(__dirname, 'src/type.ts'),
},
name: 'BaotaUtils',
formats: ['es', 'cjs'],
fileName: (format, entryName) => `${entryName}.${format === 'es' ? 'mjs' : 'cjs'}`,
},
rollupOptions: {
external: ['jsencrypt', 'md5', 'ramda'],
output: {
globals: {
jsencrypt: 'JSEncrypt',
md5: 'md5',
ramda: 'R',
},
},
},
},
})