304 lines
8.5 KiB
TypeScript
304 lines
8.5 KiB
TypeScript
/**
|
||
* 节流函数 - 限制函数执行频率
|
||
* @param func 需要节流的函数
|
||
* @param delay 节流延迟时间(毫秒)
|
||
* @returns 节流后的函数
|
||
*/
|
||
export function throttle<T extends (...args: any[]) => any>(
|
||
func: T,
|
||
delay: number = 300
|
||
): (...args: Parameters<T>) => void {
|
||
let lastExecTime = 0
|
||
|
||
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
|
||
const now = Date.now()
|
||
|
||
// 只有超过延迟时间才执行
|
||
if (now - lastExecTime >= delay) {
|
||
func.apply(this, args)
|
||
lastExecTime = now
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 防抖函数
|
||
* @param func 需要防抖的函数
|
||
* @param delay 延迟时间(毫秒)
|
||
* @returns 防抖后的函数
|
||
*/
|
||
export function debounce<T extends (...args: any[]) => any>(
|
||
func: T,
|
||
delay: number = 300
|
||
): (...args: Parameters<T>) => void {
|
||
let timeoutId: NodeJS.Timeout | null = null
|
||
|
||
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
|
||
if (timeoutId) {
|
||
clearTimeout(timeoutId)
|
||
}
|
||
|
||
timeoutId = setTimeout(() => {
|
||
func.apply(this, args)
|
||
timeoutId = null
|
||
}, delay)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将文件路径数组转换为对象数组
|
||
* @param filePaths 文件路径数组或单个文件路径字符串
|
||
* @returns 包含文件信息的对象数组
|
||
*/
|
||
export function convertFilePathsToObject(filePaths: string | string[]): Array<{
|
||
url: string
|
||
name: string
|
||
domain: string
|
||
uuid: string
|
||
extension: string // 新增后缀名字段
|
||
}> {
|
||
// 如果是字符串,先转换为数组
|
||
const paths = Array.isArray(filePaths) ? filePaths : [filePaths]
|
||
|
||
return paths.map((path) => {
|
||
// 确保path是字符串类型
|
||
if (typeof path !== 'string') {
|
||
return {
|
||
url: '',
|
||
domain: '',
|
||
name: '',
|
||
uuid: '',
|
||
extension: '' // 新增后缀名字段
|
||
}
|
||
}
|
||
|
||
// 自动补全协议头
|
||
let fullPath = path
|
||
if (!path.startsWith('http://') && !path.startsWith('https://')) {
|
||
fullPath = `http://${path}`
|
||
}
|
||
|
||
// 分割域名和文件部分
|
||
const urlObj = new URL(fullPath)
|
||
const domain = urlObj.origin
|
||
const filePart = urlObj.pathname.substring(1) // 移除开头的 /
|
||
|
||
// 优化文件名解析逻辑
|
||
// 对于路径 "cd78824383f7新建 文本文档.txt"
|
||
// 我们不再强制要求下划线分隔格式,而是提取完整的文件名
|
||
let name = ''
|
||
let uuid = ''
|
||
let extension = '' // 新增后缀名变量
|
||
|
||
// 尝试从文件名中提取 UUID(假设 UUID 是文件名开头的一段字母数字组合)
|
||
const uuidMatch = filePart.match(/^([a-f0-9]+)/i)
|
||
if (uuidMatch && uuidMatch[1]) {
|
||
uuid = uuidMatch[1]
|
||
// 文件名是去掉 UUID 后的部分
|
||
name = filePart.substring(uuid.length)
|
||
// 如果文件名以斜杠开头则去除
|
||
if (name.startsWith('/')) {
|
||
name = name.substring(1)
|
||
}
|
||
} else {
|
||
// 如果没有匹配到 UUID 格式,整个作为文件名处理
|
||
name = filePart
|
||
}
|
||
|
||
// 如果 name 为空,则使用完整文件部分作为文件名
|
||
if (!name) {
|
||
name = filePart
|
||
}
|
||
|
||
// 提取文件扩展名
|
||
const lastDotIndex = name.lastIndexOf('.')
|
||
if (lastDotIndex > 0) {
|
||
// 确保点号不在开头
|
||
extension = name.substring(lastDotIndex + 1).toLowerCase()
|
||
name = name.substring(0, lastDotIndex) // 移除扩展名部分,只保留文件名
|
||
}
|
||
|
||
// 解码 URL 编码的文件名
|
||
try {
|
||
name = decodeURIComponent(name)
|
||
} catch (e) {
|
||
// 如果解码失败,保持原始文件名
|
||
console.warn('文件名解码失败:', name)
|
||
}
|
||
|
||
return {
|
||
url: fullPath,
|
||
domain,
|
||
name,
|
||
uuid,
|
||
extension // 新增后缀名字段
|
||
}
|
||
})
|
||
}
|
||
|
||
// 安全的文件信息提取函数
|
||
// 更新返回类型定义
|
||
export function getFileInfo(fileStr: string): {
|
||
url: string
|
||
name: string
|
||
extension: string // 新增扩展名字段
|
||
} | null {
|
||
try {
|
||
if (!fileStr) return null
|
||
|
||
// 如果是有效的JSON字符串,按原有逻辑处理
|
||
if (isValidJson(fileStr)) {
|
||
const parsed = JSON.parse(fileStr)
|
||
const files = convertFilePathsToObject(parsed)
|
||
return files && files.length > 0
|
||
? {
|
||
url: files[0].url,
|
||
name: files[0].name,
|
||
extension: files[0].extension // 新增扩展名
|
||
}
|
||
: null
|
||
}
|
||
// 如果是URL字符串,直接使用 convertFilePathsToObject 处理
|
||
else if (typeof fileStr === 'string') {
|
||
const files = convertFilePathsToObject(fileStr)
|
||
return files && files.length > 0
|
||
? {
|
||
url: files[0].url,
|
||
name: files[0].name,
|
||
extension: files[0].extension // 新增扩展名
|
||
}
|
||
: null
|
||
}
|
||
|
||
return null
|
||
} catch (e) {
|
||
console.error('解析文件信息失败:', e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
export function deepCloneByJSON(obj: any) {
|
||
try {
|
||
return JSON.parse(JSON.stringify(obj))
|
||
} catch (error) {
|
||
console.error('深拷贝失败:', error)
|
||
return obj
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将 ISO 8601 时间格式转换为指定格式
|
||
* @param isoString ISO 8601 格式的时间字符串,如 "2026-02-28T14:10:51.966269"
|
||
* @param format 格式化模板,默认 'YYYY-MM-DD HH:mm:ss'
|
||
* @returns 格式化后的时间字符串
|
||
*
|
||
* @example
|
||
* formatISOToDateTime('2026-02-28T14:10:51.966269') // '2026-02-28 14:10:51'
|
||
* formatISOToDateTime('2026-02-28T14:10:51.966269', 'YYYY-MM-DD') // '2026-02-28'
|
||
* formatISOToDateTime('2026-02-28T14:10:51.966269', 'YYYY/MM/DD HH:mm') // '2026/02/28 14:10'
|
||
* formatISOToDateTime('2026-02-28T14:10:51.966269', 'MM-DD HH:mm') // '02-28 14:10'
|
||
* formatISOToDateTime('2026-02-28T14:10:51.966269', 'YYYY 年 MM 月 DD 日') // '2026 年 02 月 28 日'
|
||
*/
|
||
export function formatISOToDateTime(isoString: string, format = 'YYYY-MM-DD HH:mm:ss'): string {
|
||
if (!isoString) return ''
|
||
|
||
try {
|
||
const date = new Date(isoString)
|
||
|
||
// 检查日期是否有效
|
||
if (isNaN(date.getTime())) {
|
||
console.warn('无效的日期格式:', isoString)
|
||
return isoString
|
||
}
|
||
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
const hours = String(date.getHours()).padStart(2, '0')
|
||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||
|
||
// 替换格式化模板中的占位符
|
||
return format
|
||
.replace('YYYY', String(year))
|
||
.replace('MM', month)
|
||
.replace('DD', day)
|
||
.replace('HH', hours)
|
||
.replace('mm', minutes)
|
||
.replace('ss', seconds)
|
||
} catch (error) {
|
||
console.error('日期格式化错误:', error)
|
||
return isoString
|
||
}
|
||
}
|
||
|
||
export function isString(value: any): boolean {
|
||
return typeof value === 'string'
|
||
}
|
||
export function isFile(obj: any): obj is File {
|
||
return obj instanceof File
|
||
}
|
||
// JSON验证
|
||
export function isValidJson(str: string): boolean {
|
||
try {
|
||
JSON.parse(str)
|
||
return true
|
||
} catch (e) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
export function isValidFormat(data: any): boolean {
|
||
let parsedData = data
|
||
|
||
// 如果输入是字符串,尝试解析为 JSON
|
||
if (typeof data === 'string') {
|
||
try {
|
||
parsedData = JSON.parse(data)
|
||
} catch (e) {
|
||
return false // JSON 解析失败,直接返回 false
|
||
}
|
||
}
|
||
|
||
// 检查是否为数组
|
||
if (!Array.isArray(parsedData)) {
|
||
return false
|
||
}
|
||
|
||
// 检查数组中的每个元素
|
||
return parsedData.every((item) => {
|
||
// 检查是否为对象且不为 null
|
||
if (typeof item !== 'object' || item === null) {
|
||
return false
|
||
}
|
||
|
||
// 检查必需的属性是否存在,且类型是否正确
|
||
const hasIndex = typeof item.index === 'number'
|
||
const hasName = typeof item.name === 'string'
|
||
const hasIdNumber = typeof item.idNumber === 'string'
|
||
|
||
return hasIndex && hasName && hasIdNumber
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 比较当前时间与指定时间(精确到秒)
|
||
* @param timeParam - 要比较的时间参数,支持日期字符串、时间戳或Date对象
|
||
* @returns 当前时间小于timeParam时返回true,否则返回false
|
||
*/
|
||
export function isCurrentTimeLessThan(timeParam: string | number | Date): boolean {
|
||
// 将传入的时间参数转换为Date对象
|
||
const paramTime = new Date(timeParam)
|
||
|
||
// 获取当前时间
|
||
const currentTime = new Date()
|
||
|
||
// 检查时间参数是否有效
|
||
if (isNaN(paramTime.getTime())) {
|
||
throw new Error('Invalid time parameter provided')
|
||
}
|
||
|
||
// 比较时间(精确到秒):当前时间 < 参数时间 返回true,否则返回false
|
||
return Math.floor(currentTime.getTime() / 1000) < Math.floor(paramTime.getTime() / 1000)
|
||
}
|