Skip to content

Latest commit

 

History

History
868 lines (761 loc) · 19.9 KB

File metadata and controls

868 lines (761 loc) · 19.9 KB

tools.js 通用工具函数文档

概述

tools.js是通用工具函数库,提供各种常用的工具方法,包括数据处理、格式化、验证、存储等功能。

文件位置: src/utils/tools.js

数据处理工具

1. 深拷贝

/**
 * 深拷贝对象
 * @param {*} obj 要拷贝的对象
 * @returns {*} 拷贝后的对象
 */
export const deepClone = (obj) => {
  if (obj === null || typeof obj !== 'object') {
    return obj
  }
  
  if (obj instanceof Date) {
    return new Date(obj.getTime())
  }
  
  if (obj instanceof Array) {
    return obj.map(item => deepClone(item))
  }
  
  if (obj instanceof RegExp) {
    return new RegExp(obj)
  }
  
  if (typeof obj === 'object') {
    const clonedObj = {}
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        clonedObj[key] = deepClone(obj[key])
      }
    }
    return clonedObj
  }
  
  return obj
}

/**
 * 使用JSON方式深拷贝(性能更好,但有限制)
 * @param {*} obj 要拷贝的对象
 * @returns {*} 拷贝后的对象
 */
export const deepCloneJSON = (obj) => {
  try {
    return JSON.parse(JSON.stringify(obj))
  } catch (error) {
    console.error('JSON深拷贝失败:', error)
    return obj
  }
}

2. 对象合并

/**
 * 深度合并对象
 * @param {Object} target 目标对象
 * @param {...Object} sources 源对象
 * @returns {Object} 合并后的对象
 */
export const deepMerge = (target, ...sources) => {
  if (!sources.length) return target
  const source = sources.shift()
  
  if (isObject(target) && isObject(source)) {
    for (const key in source) {
      if (isObject(source[key])) {
        if (!target[key]) Object.assign(target, { [key]: {} })
        deepMerge(target[key], source[key])
      } else {
        Object.assign(target, { [key]: source[key] })
      }
    }
  }
  
  return deepMerge(target, ...sources)
}

/**
 * 检查是否为对象
 * @param {*} item 要检查的项
 * @returns {boolean} 是否为对象
 */
const isObject = (item) => {
  return item && typeof item === 'object' && !Array.isArray(item)
}

3. 数组处理

/**
 * 数组去重
 * @param {Array} arr 要去重的数组
 * @param {string} key 对象数组的去重键
 * @returns {Array} 去重后的数组
 */
export const uniqueArray = (arr, key = null) => {
  if (!Array.isArray(arr)) return []
  
  if (key) {
    // 对象数组去重
    const seen = new Set()
    return arr.filter(item => {
      const value = item[key]
      if (seen.has(value)) {
        return false
      }
      seen.add(value)
      return true
    })
  } else {
    // 基本类型数组去重
    return [...new Set(arr)]
  }
}

/**
 * 数组分组
 * @param {Array} arr 要分组的数组
 * @param {string|Function} key 分组键或分组函数
 * @returns {Object} 分组后的对象
 */
export const groupBy = (arr, key) => {
  if (!Array.isArray(arr)) return {}
  
  return arr.reduce((groups, item) => {
    const groupKey = typeof key === 'function' ? key(item) : item[key]
    if (!groups[groupKey]) {
      groups[groupKey] = []
    }
    groups[groupKey].push(item)
    return groups
  }, {})
}

/**
 * 数组分块
 * @param {Array} arr 要分块的数组
 * @param {number} size 每块的大小
 * @returns {Array} 分块后的二维数组
 */
export const chunk = (arr, size) => {
  if (!Array.isArray(arr) || size <= 0) return []
  
  const chunks = []
  for (let i = 0; i < arr.length; i += size) {
    chunks.push(arr.slice(i, i + size))
  }
  return chunks
}

/**
 * 数组扁平化
 * @param {Array} arr 要扁平化的数组
 * @param {number} depth 扁平化深度
 * @returns {Array} 扁平化后的数组
 */
export const flatten = (arr, depth = 1) => {
  if (!Array.isArray(arr)) return []
  
  return depth > 0 
    ? arr.reduce((acc, val) => 
        acc.concat(Array.isArray(val) ? flatten(val, depth - 1) : val), [])
    : arr.slice()
}

格式化工具

1. 日期格式化

/**
 * 格式化日期
 * @param {Date|string|number} date 日期
 * @param {string} format 格式字符串
 * @returns {string} 格式化后的日期字符串
 */
export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
  if (!date) return ''
  
  const d = new Date(date)
  if (isNaN(d.getTime())) return ''
  
  const year = d.getFullYear()
  const month = String(d.getMonth() + 1).padStart(2, '0')
  const day = String(d.getDate()).padStart(2, '0')
  const hours = String(d.getHours()).padStart(2, '0')
  const minutes = String(d.getMinutes()).padStart(2, '0')
  const seconds = String(d.getSeconds()).padStart(2, '0')
  
  return format
    .replace('YYYY', year)
    .replace('MM', month)
    .replace('DD', day)
    .replace('HH', hours)
    .replace('mm', minutes)
    .replace('ss', seconds)
}

/**
 * 相对时间格式化
 * @param {Date|string|number} date 日期
 * @returns {string} 相对时间字符串
 */
export const formatRelativeTime = (date) => {
  if (!date) return ''
  
  const now = new Date()
  const target = new Date(date)
  const diff = now.getTime() - target.getTime()
  
  const minute = 60 * 1000
  const hour = 60 * minute
  const day = 24 * hour
  const week = 7 * day
  const month = 30 * day
  const year = 365 * day
  
  if (diff < minute) {
    return '刚刚'
  } else if (diff < hour) {
    return `${Math.floor(diff / minute)}分钟前`
  } else if (diff < day) {
    return `${Math.floor(diff / hour)}小时前`
  } else if (diff < week) {
    return `${Math.floor(diff / day)}天前`
  } else if (diff < month) {
    return `${Math.floor(diff / week)}周前`
  } else if (diff < year) {
    return `${Math.floor(diff / month)}个月前`
  } else {
    return `${Math.floor(diff / year)}年前`
  }
}

2. 数字格式化

/**
 * 格式化数字(千分位分隔)
 * @param {number} num 数字
 * @param {number} decimals 小数位数
 * @returns {string} 格式化后的数字字符串
 */
export const formatNumber = (num, decimals = 0) => {
  if (isNaN(num)) return '0'
  
  return Number(num).toLocaleString('zh-CN', {
    minimumFractionDigits: decimals,
    maximumFractionDigits: decimals
  })
}

/**
 * 格式化文件大小
 * @param {number} bytes 字节数
 * @param {number} decimals 小数位数
 * @returns {string} 格式化后的文件大小
 */
export const formatFileSize = (bytes, decimals = 2) => {
  if (bytes === 0) return '0 B'
  
  const k = 1024
  const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  
  return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]
}

/**
 * 格式化百分比
 * @param {number} num 数字
 * @param {number} decimals 小数位数
 * @returns {string} 百分比字符串
 */
export const formatPercent = (num, decimals = 2) => {
  if (isNaN(num)) return '0%'
  return (Number(num) * 100).toFixed(decimals) + '%'
}

3. 字符串处理

/**
 * 首字母大写
 * @param {string} str 字符串
 * @returns {string} 首字母大写的字符串
 */
export const capitalize = (str) => {
  if (!str) return ''
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
}

/**
 * 驼峰转短横线
 * @param {string} str 驼峰字符串
 * @returns {string} 短横线字符串
 */
export const camelToKebab = (str) => {
  return str.replace(/([A-Z])/g, '-$1').toLowerCase()
}

/**
 * 短横线转驼峰
 * @param {string} str 短横线字符串
 * @returns {string} 驼峰字符串
 */
export const kebabToCamel = (str) => {
  return str.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase())
}

/**
 * 截断字符串
 * @param {string} str 字符串
 * @param {number} length 最大长度
 * @param {string} suffix 后缀
 * @returns {string} 截断后的字符串
 */
export const truncate = (str, length = 50, suffix = '...') => {
  if (!str || str.length <= length) return str
  return str.substring(0, length) + suffix
}

/**
 * 移除HTML标签
 * @param {string} html HTML字符串
 * @returns {string} 纯文本字符串
 */
export const stripHtml = (html) => {
  if (!html) return ''
  return html.replace(/<[^>]*>/g, '')
}

验证工具

1. 数据类型验证

/**
 * 检查是否为空值
 * @param {*} value 要检查的值
 * @returns {boolean} 是否为空
 */
export const isEmpty = (value) => {
  if (value === null || value === undefined) return true
  if (typeof value === 'string') return value.trim() === ''
  if (Array.isArray(value)) return value.length === 0
  if (typeof value === 'object') return Object.keys(value).length === 0
  return false
}

/**
 * 检查是否为有效的邮箱
 * @param {string} email 邮箱地址
 * @returns {boolean} 是否为有效邮箱
 */
export const isValidEmail = (email) => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return emailRegex.test(email)
}

/**
 * 检查是否为有效的手机号
 * @param {string} phone 手机号
 * @returns {boolean} 是否为有效手机号
 */
export const isValidPhone = (phone) => {
  const phoneRegex = /^1[3-9]\d{9}$/
  return phoneRegex.test(phone)
}

/**
 * 检查是否为有效的身份证号
 * @param {string} idCard 身份证号
 * @returns {boolean} 是否为有效身份证号
 */
export const isValidIdCard = (idCard) => {
  const idCardRegex = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
  return idCardRegex.test(idCard)
}

/**
 * 检查是否为有效的URL
 * @param {string} url URL地址
 * @returns {boolean} 是否为有效URL
 */
export const isValidUrl = (url) => {
  try {
    new URL(url)
    return true
  } catch {
    return false
  }
}

2. 密码强度验证

/**
 * 检查密码强度
 * @param {string} password 密码
 * @returns {Object} 密码强度信息
 */
export const checkPasswordStrength = (password) => {
  if (!password) {
    return { level: 0, text: '请输入密码' }
  }
  
  let score = 0
  const checks = {
    length: password.length >= 8,
    lowercase: /[a-z]/.test(password),
    uppercase: /[A-Z]/.test(password),
    number: /\d/.test(password),
    special: /[!@#$%^&*(),.?":{}|<>]/.test(password)
  }
  
  // 计算得分
  Object.values(checks).forEach(check => {
    if (check) score++
  })
  
  // 长度加分
  if (password.length >= 12) score++
  
  // 确定强度等级
  let level = 0
  let text = '弱'
  let color = '#ff4d4f'
  
  if (score >= 6) {
    level = 3
    text = '强'
    color = '#52c41a'
  } else if (score >= 4) {
    level = 2
    text = '中'
    color = '#faad14'
  } else if (score >= 2) {
    level = 1
    text = '弱'
    color = '#ff7875'
  }
  
  return {
    level,
    text,
    color,
    score,
    checks,
    suggestions: generatePasswordSuggestions(checks)
  }
}

/**
 * 生成密码建议
 * @param {Object} checks 检查结果
 * @returns {Array} 建议列表
 */
const generatePasswordSuggestions = (checks) => {
  const suggestions = []
  
  if (!checks.length) suggestions.push('密码长度至少8位')
  if (!checks.lowercase) suggestions.push('包含小写字母')
  if (!checks.uppercase) suggestions.push('包含大写字母')
  if (!checks.number) suggestions.push('包含数字')
  if (!checks.special) suggestions.push('包含特殊字符')
  
  return suggestions
}

存储工具

1. localStorage封装

/**
 * localStorage存储
 * @param {string} key 键
 * @param {*} value 值
 * @param {number} expire 过期时间(毫秒)
 */
export const setStorage = (key, value, expire = null) => {
  try {
    const data = {
      value,
      expire: expire ? Date.now() + expire : null
    }
    localStorage.setItem(key, JSON.stringify(data))
  } catch (error) {
    console.error('存储数据失败:', error)
  }
}

/**
 * localStorage获取
 * @param {string} key 键
 * @returns {*} 值
 */
export const getStorage = (key) => {
  try {
    const item = localStorage.getItem(key)
    if (!item) return null
    
    const data = JSON.parse(item)
    
    // 检查是否过期
    if (data.expire && Date.now() > data.expire) {
      localStorage.removeItem(key)
      return null
    }
    
    return data.value
  } catch (error) {
    console.error('获取存储数据失败:', error)
    return null
  }
}

/**
 * localStorage删除
 * @param {string} key 键
 */
export const removeStorage = (key) => {
  try {
    localStorage.removeItem(key)
  } catch (error) {
    console.error('删除存储数据失败:', error)
  }
}

/**
 * localStorage清空
 */
export const clearStorage = () => {
  try {
    localStorage.clear()
  } catch (error) {
    console.error('清空存储数据失败:', error)
  }
}

2. sessionStorage封装

/**
 * sessionStorage存储
 * @param {string} key 键
 * @param {*} value 值
 */
export const setSessionStorage = (key, value) => {
  try {
    sessionStorage.setItem(key, JSON.stringify(value))
  } catch (error) {
    console.error('存储会话数据失败:', error)
  }
}

/**
 * sessionStorage获取
 * @param {string} key 键
 * @returns {*} 值
 */
export const getSessionStorage = (key) => {
  try {
    const item = sessionStorage.getItem(key)
    return item ? JSON.parse(item) : null
  } catch (error) {
    console.error('获取会话数据失败:', error)
    return null
  }
}

/**
 * sessionStorage删除
 * @param {string} key 键
 */
export const removeSessionStorage = (key) => {
  try {
    sessionStorage.removeItem(key)
  } catch (error) {
    console.error('删除会话数据失败:', error)
  }
}

浏览器工具

1. 设备检测

/**
 * 获取设备信息
 * @returns {Object} 设备信息
 */
export const getDeviceInfo = () => {
  const ua = navigator.userAgent
  
  return {
    isMobile: /Mobile|Android|iPhone|iPad/.test(ua),
    isTablet: /iPad|Tablet/.test(ua),
    isDesktop: !/Mobile|Android|iPhone|iPad|Tablet/.test(ua),
    isIOS: /iPhone|iPad|iPod/.test(ua),
    isAndroid: /Android/.test(ua),
    isWeChat: /MicroMessenger/.test(ua),
    browser: getBrowserInfo(),
    os: getOSInfo()
  }
}

/**
 * 获取浏览器信息
 * @returns {Object} 浏览器信息
 */
const getBrowserInfo = () => {
  const ua = navigator.userAgent
  
  if (ua.includes('Chrome')) return { name: 'Chrome', version: getVersion(ua, 'Chrome') }
  if (ua.includes('Firefox')) return { name: 'Firefox', version: getVersion(ua, 'Firefox') }
  if (ua.includes('Safari')) return { name: 'Safari', version: getVersion(ua, 'Safari') }
  if (ua.includes('Edge')) return { name: 'Edge', version: getVersion(ua, 'Edge') }
  
  return { name: 'Unknown', version: '' }
}

/**
 * 获取操作系统信息
 * @returns {Object} 操作系统信息
 */
const getOSInfo = () => {
  const ua = navigator.userAgent
  
  if (ua.includes('Windows')) return { name: 'Windows', version: '' }
  if (ua.includes('Mac OS')) return { name: 'macOS', version: '' }
  if (ua.includes('Linux')) return { name: 'Linux', version: '' }
  if (ua.includes('Android')) return { name: 'Android', version: '' }
  if (ua.includes('iOS')) return { name: 'iOS', version: '' }
  
  return { name: 'Unknown', version: '' }
}

/**
 * 获取版本号
 * @param {string} ua 用户代理字符串
 * @param {string} browser 浏览器名称
 * @returns {string} 版本号
 */
const getVersion = (ua, browser) => {
  const regex = new RegExp(`${browser}\/([\\d\\.]+)`)
  const match = ua.match(regex)
  return match ? match[1] : ''
}

2. 剪贴板操作

/**
 * 复制文本到剪贴板
 * @param {string} text 要复制的文本
 * @returns {Promise<boolean>} 是否成功
 */
export const copyToClipboard = async (text) => {
  try {
    if (navigator.clipboard) {
      await navigator.clipboard.writeText(text)
      return true
    } else {
      // 降级方案
      const textArea = document.createElement('textarea')
      textArea.value = text
      textArea.style.position = 'fixed'
      textArea.style.opacity = '0'
      document.body.appendChild(textArea)
      textArea.select()
      const success = document.execCommand('copy')
      document.body.removeChild(textArea)
      return success
    }
  } catch (error) {
    console.error('复制失败:', error)
    return false
  }
}

/**
 * 从剪贴板读取文本
 * @returns {Promise<string>} 剪贴板文本
 */
export const readFromClipboard = async () => {
  try {
    if (navigator.clipboard) {
      return await navigator.clipboard.readText()
    }
    return ''
  } catch (error) {
    console.error('读取剪贴板失败:', error)
    return ''
  }
}

性能工具

1. 防抖和节流

/**
 * 防抖函数
 * @param {Function} func 要防抖的函数
 * @param {number} delay 延迟时间
 * @returns {Function} 防抖后的函数
 */
export const debounce = (func, delay = 300) => {
  let timeoutId
  return function (...args) {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => func.apply(this, args), delay)
  }
}

/**
 * 节流函数
 * @param {Function} func 要节流的函数
 * @param {number} delay 延迟时间
 * @returns {Function} 节流后的函数
 */
export const throttle = (func, delay = 300) => {
  let lastTime = 0
  return function (...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      lastTime = now
      func.apply(this, args)
    }
  }
}

2. 性能监控

/**
 * 测量函数执行时间
 * @param {Function} func 要测量的函数
 * @param {string} label 标签
 * @returns {*} 函数返回值
 */
export const measureTime = async (func, label = 'Function') => {
  const start = performance.now()
  const result = await func()
  const end = performance.now()
  console.log(`${label} 执行时间: ${(end - start).toFixed(2)}ms`)
  return result
}

/**
 * 获取页面性能信息
 * @returns {Object} 性能信息
 */
export const getPerformanceInfo = () => {
  if (!window.performance) return null
  
  const timing = performance.timing
  const navigation = performance.navigation
  
  return {
    // 页面加载时间
    loadTime: timing.loadEventEnd - timing.navigationStart,
    // DNS查询时间
    dnsTime: timing.domainLookupEnd - timing.domainLookupStart,
    // TCP连接时间
    tcpTime: timing.connectEnd - timing.connectStart,
    // 请求时间
    requestTime: timing.responseEnd - timing.requestStart,
    // 解析DOM时间
    domTime: timing.domComplete - timing.domLoading,
    // 白屏时间
    whiteScreenTime: timing.responseStart - timing.navigationStart,
    // 首屏时间
    firstScreenTime: timing.loadEventEnd - timing.navigationStart,
    // 导航类型
    navigationType: navigation.type,
    // 重定向次数
    redirectCount: navigation.redirectCount
  }
}

使用示例

1. 基本使用

import { 
  deepClone, 
  formatDate, 
  isValidEmail, 
  debounce,
  copyToClipboard 
} from '@/utils/tools'

// 深拷贝
const originalData = { name: 'John', age: 30 }
const clonedData = deepClone(originalData)

// 日期格式化
const formattedDate = formatDate(new Date(), 'YYYY-MM-DD')

// 邮箱验证
const isValid = isValidEmail('user@example.com')

// 防抖搜索
const debouncedSearch = debounce((keyword) => {
  console.log('搜索:', keyword)
}, 500)

// 复制到剪贴板
copyToClipboard('Hello World').then(success => {
  if (success) {
    console.log('复制成功')
  }
})

2. 在组件中使用

<script setup>
import { ref, computed } from 'vue'
import { formatFileSize, checkPasswordStrength, getDeviceInfo } from '@/utils/tools'

const fileSize = ref(1024000)
const password = ref('')

// 格式化文件大小
const formattedSize = computed(() => formatFileSize(fileSize.value))

// 密码强度检查
const passwordStrength = computed(() => checkPasswordStrength(password.value))

// 设备信息
const deviceInfo = getDeviceInfo()
console.log('设备信息:', deviceInfo)
</script>

注意事项

  1. 性能: 避免在循环中使用复杂的工具函数
  2. 兼容性: 考虑不同浏览器的兼容性
  3. 错误处理: 添加适当的错误处理和降级方案
  4. 类型检查: 对输入参数进行类型检查
  5. 文档: 为每个函数提供清晰的文档说明

相关文档


最后更新时间:2025-09-19