前言
每个 Vue3 项目都会积累一批通用工具方法,与其在各组件里零散定义,不如统一收口到 utils/ 目录。本文整理了一套开箱即用的工具函数,覆盖树结构、文本操作、数据处理等高频场景。
使用方式
1 2 3 4 5 6 7 8 9
| export { listToTree, treeToList } from './tree' export { copyText } from './clipboard' export { classifyByField } from './data' export { debounce, throttle } from './performance' export { deepClone } from './clone' export { formatDate, formatMoney } from './format' export { downloadFile, getUrlParams } from './browser' export { uniqueBy, phoneMask, idCardMask } from './common'
|
在组件中按需引入:
1 2 3
| <script setup> import { listToTree, debounce, copyText } from '@/utils' </script>
|
一、列表 ↔ 树结构互转
listToTree — 列表转树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
export function listToTree(list, options = {}) { const { id = 'id', pid = 'pid', children = 'children', rootValue = null } = options
const map = {} const tree = []
list.forEach(item => { map[item[id]] = { ...item, [children]: [] } })
list.forEach(item => { const node = map[item[id]] const parent = map[item[pid]]
if (parent) { parent[children].push(node) } else if (item[pid] === rootValue) { tree.push(node) } })
return tree }
|
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const list = [ { id: 1, pid: null, name: '总公司' }, { id: 2, pid: 1, name: '研发部' }, { id: 3, pid: 1, name: '市场部' }, { id: 4, pid: 2, name: '前端组' }, { id: 5, pid: 2, name: '后端组' } ]
const tree = listToTree(list)
const tree2 = listToTree(list, { id: 'id', pid: 'pid', children: 'sub' })
|
treeToList — 树转列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
export function treeToList(tree, childrenKey = 'children', keepChildren = false) { const result = []
function traverse(nodes, parentId = null, level = 0) { nodes.forEach(node => { const children = node[childrenKey] const item = { ...node }
if (!keepChildren) delete item[childrenKey]
item._level = level item._parentId = parentId
result.push(item)
if (children && children.length > 0) { traverse(children, node.id, level + 1) } }) }
traverse(tree) return result }
|
使用示例:
1 2 3 4 5
| const flatList = treeToList(tree)
const level1Nodes = flatList.filter(n => n._level === 1)
|
递归查找树节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
export function findNode(tree, predicate) { for (const node of tree) { if (predicate(node)) return node if (node.children) { const found = findNode(node.children, predicate) if (found) return found } } return null }
|
二、复制文本内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
export async function copyText(text) { try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text) return true }
const textarea = document.createElement('textarea') textarea.value = text textarea.style.position = 'fixed' textarea.style.left = '-9999px' textarea.style.top = '-9999px' document.body.appendChild(textarea) textarea.focus() textarea.select()
const success = document.execCommand('copy') document.body.removeChild(textarea) return success } catch (err) { console.error('复制失败:', err) return false } }
|
在 Vue3 组件中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script setup> import { copyText } from '@/utils'
async function handleCopy(row) { const ok = await copyText(row.url) // 使用 ant-design-vue message 提示 message.success(ok ? '复制成功' : '复制失败') } </script>
<template> <button @click="handleCopy({ url: 'https://example.com' })"> 复制链接 </button> </template>
|
三、根据数组对象某个字段分类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
export function classifyByField(arr, field) { return arr.reduce((acc, item) => { const key = item[field] if (!acc[key]) acc[key] = [] acc[key].push(item) return acc }, {}) }
|
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const roles = [ { name: '张三', dept: '研发部', level: '高级' }, { name: '李四', dept: '市场部', level: '中级' }, { name: '王五', dept: '研发部', level: '初级' }, { name: '赵六', dept: '市场部', level: '高级' } ]
const byDept = classifyByField(roles, 'dept')
const byLevel = classifyByField(roles, 'level')
|
扩展:分类生成树结构(两级筛选器用):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
export function classifyToOptions(arr, typeField, labelField = 'name') { const groups = classifyByField(arr, typeField) return Object.entries(groups).map(([key, items]) => ({ label: key, value: key, children: items.map(item => ({ label: item[labelField], value: item[labelField] })) })) }
|
四、防抖与节流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
export function debounce(fn, delay = 300, immediate = false) { let timer = null return function (...args) { const callNow = immediate && !timer clearTimeout(timer) timer = setTimeout(() => { timer = null !immediate && fn.apply(this, args) }, delay) if (callNow) fn.apply(this, args) } }
export function throttle(fn, interval = 300) { let lastTime = 0 return function (...args) { const now = Date.now() if (now - lastTime >= interval) { fn.apply(this, args) lastTime = now } } }
|
Vue3 中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <script setup> import { debounce, throttle } from '@/utils'
// 搜索输入防抖 const handleSearch = debounce((keyword) => { fetchResults(keyword) }, 500)
// 滚动加载节流 const handleScroll = throttle(() => { console.log('滚动中...') }, 200) </script>
|
五、深拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
export function deepClone(target, hash = new WeakMap()) { if (target === null || typeof target !== 'object') return target
if (hash.has(target)) return hash.get(target)
if (target instanceof Date) return new Date(target) if (target instanceof RegExp) return new RegExp(target)
if (target instanceof Map) { const map = new Map() hash.set(target, map) target.forEach((v, k) => map.set(k, deepClone(v, hash))) return map }
if (target instanceof Set) { const set = new Set() hash.set(target, set) target.forEach(v => set.add(deepClone(v, hash))) return set }
const clone = Array.isArray(target) ? [] : {} hash.set(target, clone)
Reflect.ownKeys(target).forEach(key => { clone[key] = deepClone(target[key], hash) })
return clone }
|
六、格式化方法
日期格式化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
export function formatDate(date, fmt = 'YYYY-MM-DD HH:mm:ss') { if (!date) return '' const d = new Date(date) if (isNaN(d.getTime())) return ''
const o = { 'Y+': d.getFullYear(), 'M+': d.getMonth() + 1, 'D+': d.getDate(), 'H+': d.getHours(), 'm+': d.getMinutes(), 's+': d.getSeconds() }
for (const [k, v] of Object.entries(o)) { fmt = fmt.replace(new RegExp(`(${k})`), match => { return String(v).padStart(match.length, '0') }) }
return fmt }
|
金额格式化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
export function formatMoney(num, decimals = 2, prefix = '¥') { if (num === null || num === undefined || isNaN(num)) return '--' const parts = Number(num).toFixed(decimals).split('.') parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',') return prefix + parts.join('.') }
|
文件大小格式化
1 2 3 4 5 6 7 8 9 10 11 12
|
export function formatFileSize(bytes) { if (bytes === 0) return '0 B' const units = ['B', 'KB', 'MB', 'GB', 'TB'] const i = Math.floor(Math.log(bytes) / Math.log(1024)) return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i] }
|
七、浏览器相关
下载文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
export function downloadFile(content, filename, type = 'text/plain') { const blob = new Blob([content], { type }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = filename document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) }
export function downloadJSON(data, filename = 'data.json') { downloadFile(JSON.stringify(data, null, 2), filename, 'application/json') }
export function downloadCSV(headers, rows, filename = 'data.csv') { const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n') const bom = '\uFEFF' downloadFile(bom + csv, filename, 'text/csv;charset=utf-8') }
|
获取 URL 参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
export function getUrlParams(key) { const params = new URLSearchParams(window.location.search) if (key) return params.get(key)
const result = {} params.forEach((value, key) => { result[key] = value }) return result }
|
滚动到指定位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
export function scrollTo(target, offset = 0) { let top = typeof target === 'number' ? target : document.querySelector(target)?.getBoundingClientRect().top + window.pageYOffset
if (top === undefined) return window.scrollTo({ top: top - offset, behavior: 'smooth' }) }
|
八、数据处理
数组对象去重
1 2 3 4 5 6 7 8 9 10 11 12 13
|
export function uniqueBy(arr, key) { const seen = new Map() return arr.filter(item => !seen.has(item[key]) && seen.set(item[key], 1)) }
|
敏感信息脱敏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
export function phoneMask(phone) { if (!phone) return '' return String(phone).replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') }
export function idCardMask(idCard) { if (!idCard) return '' const str = String(idCard) return str.slice(0, 3) + '*'.repeat(str.length - 7) + str.slice(-4) }
export function emailMask(email) { if (!email) return '' const [name, domain] = email.split('@') return name[0] + '***' + (name.length > 1 ? name.slice(-1) : '') + '@' + domain }
export function nameMask(name) { if (!name) return '' return name[0] + '*'.repeat(name.length - 1) }
|
随机数 / 随机颜色
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
export function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min }
export function randomColor() { return '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0') }
|
排序(支持中文和嵌套字段)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
export function sortBy(arr, field, order = 'asc') { const dir = order === 'asc' ? 1 : -1
return [...arr].sort((a, b) => { const valA = field.split('.').reduce((o, k) => o?.[k], a) const valB = field.split('.').reduce((o, k) => o?.[k], b)
if (typeof valA === 'string') { return valA.localeCompare(valB) * dir } return (valA - valB) * dir }) }
|
九、统一导出总入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
export { listToTree, treeToList, findNode } from './tree'
export { copyText } from './clipboard'
export { classifyByField, classifyToOptions } from './data' export { uniqueBy, phoneMask, idCardMask, emailMask, nameMask } from './common' export { sortBy, randomInt, randomColor } from './common'
export { debounce, throttle } from './performance'
export { deepClone } from './clone'
export { formatDate, formatMoney, formatFileSize } from './format'
export { downloadFile, downloadJSON, downloadCSV, getUrlParams, scrollTo } from './browser'
|
总结
| 分类 |
方法 |
用途 |
| 树结构 |
listToTree treeToList findNode |
递归结构互转与查找 |
| 剪贴板 |
copyText |
一键复制文本 |
| 数据分组 |
classifyByField classifyToOptions |
按字段分类、级联选项生成 |
| 性能 |
debounce throttle |
搜索防抖、滚动节流 |
| 克隆 |
deepClone |
深拷贝支持循环引用 |
| 格式化 |
formatDate formatMoney formatFileSize |
日期、金额、文件大小 |
| 浏览器 |
downloadFile getUrlParams scrollTo |
下载、URL 参数、滚动 |
| 脱敏 |
phoneMask idCardMask emailMask |
敏感信息保护 |
| 工具 |
uniqueBy sortBy randomInt |
去重、排序、随机数 |
所有方法纯 JS 实现,不依赖任何框架,Vue3 / React / 原生项目都能用。复制到项目 utils/ 目录下即用。
内容由 AI 生成,仅供参考。本文发布于 码上学习,转载请注明出处。