tools.js是通用工具函数库,提供各种常用的工具方法,包括数据处理、格式化、验证、存储等功能。
文件位置: src/utils/tools.js
/**
* 深拷贝对象
* @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
}
}/**
* 深度合并对象
* @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)
}/**
* 数组去重
* @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()
}/**
* 格式化日期
* @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)}年前`
}
}/**
* 格式化数字(千分位分隔)
* @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) + '%'
}/**
* 首字母大写
* @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, '')
}/**
* 检查是否为空值
* @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
}
}/**
* 检查密码强度
* @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
}/**
* 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)
}
}/**
* 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)
}
}/**
* 获取设备信息
* @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] : ''
}/**
* 复制文本到剪贴板
* @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 ''
}
}/**
* 防抖函数
* @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)
}
}
}/**
* 测量函数执行时间
* @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
}
}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('复制成功')
}
})<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>- 性能: 避免在循环中使用复杂的工具函数
- 兼容性: 考虑不同浏览器的兼容性
- 错误处理: 添加适当的错误处理和降级方案
- 类型检查: 对输入参数进行类型检查
- 文档: 为每个函数提供清晰的文档说明
最后更新时间:2025-09-19