mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-08 07:41:10 +08:00
【同步】前端项目源码
【修复】工作流兼容问题
This commit is contained in:
BIN
frontend/packages/utils/.DS_Store
vendored
Normal file
BIN
frontend/packages/utils/.DS_Store
vendored
Normal file
Binary file not shown.
148
frontend/packages/utils/docs/browser.md
Normal file
148
frontend/packages/utils/docs/browser.md
Normal 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. 所有清除缓存的操作都是不可逆的,请谨慎使用
|
||||
219
frontend/packages/utils/docs/business.md
Normal file
219
frontend/packages/utils/docs/business.md
Normal 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. 所有函数都经过优化,支持函数式编程范式
|
||||
274
frontend/packages/utils/docs/data.md
Normal file
274
frontend/packages/utils/docs/data.md
Normal 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. 映射表中的路径必须存在于源数据中
|
||||
198
frontend/packages/utils/docs/date.md
Normal file
198
frontend/packages/utils/docs/date.md
Normal 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. 使用柯里化函数时注意参数顺序
|
||||
132
frontend/packages/utils/docs/encipher.md
Normal file
132
frontend/packages/utils/docs/encipher.md
Normal 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 位)
|
||||
135
frontend/packages/utils/docs/random.md
Normal file
135
frontend/packages/utils/docs/random.md
Normal 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
|
||||
139
frontend/packages/utils/docs/string.md
Normal file
139
frontend/packages/utils/docs/string.md
Normal 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)
|
||||
|
||||
支持的转义字符:
|
||||
|
||||
- & -> &
|
||||
- < -> <
|
||||
- > -> >
|
||||
- " -> "
|
||||
- ' -> '
|
||||
|
||||
示例:
|
||||
|
||||
```typescript
|
||||
// 转义
|
||||
htmlEscape('<div>Hello & World</div>')
|
||||
// 返回: '<div>Hello & World</div>'
|
||||
|
||||
// 反转义
|
||||
htmlEscape('<div>Hello & World</div>', 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. 所有函数都是纯函数,不会修改原始数据
|
||||
247
frontend/packages/utils/docs/type.md
Normal file
247
frontend/packages/utils/docs/type.md
Normal 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. 所有函数都是类型安全的
|
||||
16
frontend/packages/utils/eslint.config.js
Normal file
16
frontend/packages/utils/eslint.config.js
Normal 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
|
||||
130
frontend/packages/utils/package.json
Normal file
130
frontend/packages/utils/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
3
frontend/packages/utils/prettier.config.js
Normal file
3
frontend/packages/utils/prettier.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import prettierConfig from '@baota/prettier'
|
||||
|
||||
export default prettierConfig
|
||||
585
frontend/packages/utils/src/browser.ts
Normal file
585
frontend/packages/utils/src/browser.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
314
frontend/packages/utils/src/business.ts
Normal file
314
frontend/packages/utils/src/business.ts
Normal 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)
|
||||
}
|
||||
300
frontend/packages/utils/src/data.ts
Normal file
300
frontend/packages/utils/src/data.ts
Normal 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)
|
||||
}
|
||||
191
frontend/packages/utils/src/date.ts
Normal file
191
frontend/packages/utils/src/date.ts
Normal 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}天` : '已过期'
|
||||
}
|
||||
86
frontend/packages/utils/src/encipher.ts
Normal file
86
frontend/packages/utils/src/encipher.ts
Normal 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
|
||||
}
|
||||
}
|
||||
99
frontend/packages/utils/src/random.ts
Normal file
99
frontend/packages/utils/src/random.ts
Normal 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('')
|
||||
}
|
||||
113
frontend/packages/utils/src/string.ts
Normal file
113
frontend/packages/utils/src/string.ts
Normal 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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
}
|
||||
// 将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()
|
||||
})
|
||||
}
|
||||
108
frontend/packages/utils/src/type.ts
Normal file
108
frontend/packages/utils/src/type.ts
Normal 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
|
||||
382
frontend/packages/utils/test/browser.spec.ts
Normal file
382
frontend/packages/utils/test/browser.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
169
frontend/packages/utils/test/business.spec.ts
Normal file
169
frontend/packages/utils/test/business.spec.ts
Normal 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]+/)
|
||||
})
|
||||
})
|
||||
})
|
||||
121
frontend/packages/utils/test/data.spec.ts
Normal file
121
frontend/packages/utils/test/data.spec.ts
Normal 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])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
123
frontend/packages/utils/test/date.spec.ts
Normal file
123
frontend/packages/utils/test/date.spec.ts
Normal 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('已过期')
|
||||
})
|
||||
})
|
||||
})
|
||||
85
frontend/packages/utils/test/encipher.spec.ts
Normal file
85
frontend/packages/utils/test/encipher.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
106
frontend/packages/utils/test/random.spec.ts
Normal file
106
frontend/packages/utils/test/random.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
75
frontend/packages/utils/test/string.spec.ts
Normal file
75
frontend/packages/utils/test/string.spec.ts
Normal 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('<div class="test">Hello & World</div>')
|
||||
})
|
||||
|
||||
it('应当正确反转义HTML字符', () => {
|
||||
const escaped = '<div class="test">Hello & World</div>'
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
102
frontend/packages/utils/test/type.spec.ts
Normal file
102
frontend/packages/utils/test/type.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
14
frontend/packages/utils/tsconfig.json
Normal file
14
frontend/packages/utils/tsconfig.json
Normal 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
0
frontend/packages/utils/types.d.ts
vendored
Normal file
45
frontend/packages/utils/vite.config.ts
Normal file
45
frontend/packages/utils/vite.config.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user