Vue3的通用方法

前言

每个 Vue3 项目都会积累一批通用工具方法,与其在各组件里零散定义,不如统一收口到 utils/ 目录。本文整理了一套开箱即用的工具函数,覆盖树结构、文本操作、数据处理等高频场景。

使用方式

1
2
3
4
5
6
7
8
9
// utils/index.js
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
// utils/tree.js

/**
* 列表数据转成树结构
* @param {Array} list — 扁平列表
* @param {Object} options — 配置项
* @param {string} options.id — id 字段名,默认 'id'
* @param {string} options.pid — 父级 id 字段名,默认 'pid'
* @param {string} options.children — 子节点字段名,默认 'children'
* @param {*} options.rootValue — 根节点的 pid 值,默认 null
* @returns {Array} 树形数据
*/
export function listToTree(list, options = {}) {
const { id = 'id', pid = 'pid', children = 'children', rootValue = null } = options

const map = {}
const tree = []

// 第一遍:建立 id → node 映射
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)
// [{ id: 1, name: '总公司', children: [
// { id: 2, name: '研发部', children: [
// { id: 4, name: '前端组' }, { id: 5, name: '后端组' }
// ]},
// { id: 3, name: '市场部' }
// ]}]

// 自定义字段名
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
/**
* 树结构转成扁平列表(保留层级关系可选)
* @param {Array} tree — 树形数据
* @param {string} childrenKey — 子节点字段名,默认 'children'
* @param {boolean} keepChildren — 是否保留 children 字段,默认 false
* @returns {Array} 扁平列表
*/
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 }

// 不保留 children
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)
// 所有节点的 children 被移除,并附加了 _level 和 _parentId

// 按层级筛出
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
/**
* 在树中递归查找节点
* @param {Array} tree — 树形数据
* @param {Function} predicate — 匹配条件,返回 boolean
* @returns {Object|null} 找到的节点
*/
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
}

// 使用:findNode(tree, node => node.id === 5)

二、复制文本内容

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
// utils/clipboard.js

/**
* 复制文本到剪贴板
* @param {string} text — 要复制的文本
* @returns {Promise<boolean>} 是否复制成功
*/
export async function copyText(text) {
try {
// 优先使用现代 API
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text)
return true
}

// 降级方案:textarea + execCommand
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
// utils/data.js

/**
* 根据指定字段将数组对象分组归类
* @param {Array} arr — 原始数组
* @param {string} field — 分类字段名
* @returns {Object} — { 字段值: [匹配项, ...], ... }
*/
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')
// {
// 研发部: [{ name:'张三',... }, { name:'王五',... }],
// 市场部: [{ name:'李四',... }, { name:'赵六',... }]
// }

// 按级别分组
const byLevel = classifyByField(roles, 'level')

扩展:分类生成树结构(两级筛选器用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 按两个字段组合生成两级树
* @param {Array} arr — 原始数组
* @param {string} typeField — 分类字段
* @param {string} labelField — 标签字段
* @returns {Array} — [{ label: '研发部', children: [{ label:'张三' }, ...] }, ...]
*/
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
// utils/performance.js

/**
* 防抖 — 最后一次触发后延迟执行
* @param {Function} fn
* @param {number} delay — 延迟毫秒数
* @param {boolean} immediate — 是否首次立即执行
*/
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)
}
}

/**
* 节流 — 固定间隔只执行一次
* @param {Function} fn
* @param {number} interval — 间隔毫秒数
*/
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
// utils/clone.js

/**
* 深拷贝(支持循环引用、Date、RegExp、Map、Set)
* @param {*} target — 要拷贝的值
* @param {WeakMap} hash — 用于处理循环引用
*/
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)

// Map
if (target instanceof Map) {
const map = new Map()
hash.set(target, map)
target.forEach((v, k) => map.set(k, deepClone(v, hash)))
return map
}

// Set
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)

// 包括 Symbol key
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
// utils/format.js

/**
* 日期格式化
* @param {Date|string|number} date
* @param {string} fmt — 格式,如 'YYYY-MM-DD HH:mm:ss'
*/
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
}

// formatDate(new Date(), 'YYYY-MM-DD') → '2026-08-07'
// formatDate(new Date(), 'HH:mm:ss') → '14:30:00'
// formatDate('2026-08-07', 'YYYY年MM月DD日') → '2026年08月07日'

金额格式化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 金额格式化(千分位 + 小数点)
* @param {number} num — 金额
* @param {number} decimals — 小数位数,默认 2
* @param {string} prefix — 前缀,如 '¥'
*/
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('.')
}

// formatMoney(1234567.89) → '¥1,234,567.89'
// formatMoney(1234567.89, 0) → '¥1,234,568'

文件大小格式化

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 文件大小格式化
* @param {number} bytes — 字节数
*/
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]
}

// formatFileSize(1536000) → '1.46 MB'

七、浏览器相关

下载文件

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
// utils/browser.js

/**
* 通过 Blob 下载文件
* @param {string} content — 文本内容
* @param {string} filename — 文件名
* @param {string} type — MIME 类型
*/
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)
}

// 导出 JSON
export function downloadJSON(data, filename = 'data.json') {
downloadFile(JSON.stringify(data, null, 2), filename, 'application/json')
}

// 导出 CSV
export function downloadCSV(headers, rows, filename = 'data.csv') {
const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n')
const bom = '\uFEFF' // BOM 解决 Excel 中文乱码
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
/**
* 获取 URL 查询参数
* @param {string} key — 参数名(可选,不传返回全部)
*/
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
}

// getUrlParams('id') → '123'
// getUrlParams() → { id: '123', name: 'test' }

滚动到指定位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 平滑滚动到指定元素或位置
* @param {string|number} target — 选择器或滚动距离
* @param {number} offset — 偏移量
*/
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' })
}

// scrollTo('#section2', 60) — 滚动到 id="section2" 距顶 60px 的位置

八、数据处理

数组对象去重

1
2
3
4
5
6
7
8
9
10
11
12
13
// utils/common.js

/**
* 根据指定字段对数组对象去重
* @param {Array} arr
* @param {string} key — 去重字段
*/
export function uniqueBy(arr, key) {
const seen = new Map()
return arr.filter(item => !seen.has(item[key]) && seen.set(item[key], 1))
}

// uniqueBy([{id:1},{id:2},{id:1}], 'id') → [{id:1},{id:2}]

敏感信息脱敏

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
/**
* 手机号脱敏 — 138****1234
*/
export function phoneMask(phone) {
if (!phone) return ''
return String(phone).replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}

/**
* 身份证脱敏 — 320***********1234
*/
export function idCardMask(idCard) {
if (!idCard) return ''
const str = String(idCard)
return str.slice(0, 3) + '*'.repeat(str.length - 7) + str.slice(-4)
}

/**
* 邮箱脱敏 — t***@example.com
*/
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
/**
* 生成指定范围随机整数 [min, max]
*/
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')
}

// randomInt(1, 100) → 42
// randomColor() → '#a3f72c'

排序(支持中文和嵌套字段)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 通用数组排序
* @param {Array} arr
* @param {string} field — 排序字段,支持 'a.b' 嵌套
* @param {string} order — 'asc' | 'desc'
*/
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
})
}

// sortBy(users, 'age', 'desc')
// sortBy(users, 'address.city', 'asc') — 支持嵌套字段

九、统一导出总入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// utils/index.js — 统一收口

// 树结构
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 生成,仅供参考。本文发布于 码上学习,转载请注明出处。