本文介绍了基于 ElementPlus 的 el-tree 组件进行二次封装的 TreeView 组件,使用 Vue3 和 JavaScript 实现。TreeView 组件通过 props 接收树形数据、配置项等,支持懒加载、节点展开/收起、节点点击、删除、编辑等操作。组件内部通过 ref 管理树实例,并提供了 clearCurrentNode、setCurrentKey、setExpandedKeys 等方法供父组件调用。renderContent 方法用于自定义节点内容,支持根据配置显示删除和编辑按钮。事件处理函数通过 emit 将节点操作传递给父组件,实现了组件与父组件的交互。样式部分通过 scoped 样式隔离,确保组件样式独立。
准备组件 TreeView treeUtils方法
- TreeView组件
<template>
<div class="tree-container">
<div v-if="isShowHeader" class="tree-header">
<slot name="header"></slot>
</div>
<el-tree
ref="treeRef"
:data="treeData"
:props="treeProps"
highlight-current
node-key="id"
:render-content="renderContent"
:lazy="lazy" :load="lazy ? loadNode : undefined"
:default-expanded-keys="expandedKeys"
:show-checkbox="showCheckbox"
:check-strictly="checkStrictly"
@node-click="handleNodeClick"
@node-expand="handleNodeExpand"
@node-collapse="handleNodeCollapse"
@check="handleCheck" />
</div>
</template>
<script setup>
import { defineProps, defineEmits, ref } from 'vue'
import { Delete, Edit } from '@element-plus/icons-vue'
import { handleNodeExpand as handleNodeExpandUtil, handleNodeCollapse as handleNodeCollapseUtil } from '@/utils/treeUtils'
// 接收父组件传来的数据
const props = defineProps({
treeData: {
type: Array,
required: true,
},
treeProps: {
type: Object,
default: () => ({
children: 'children',
label: 'label',
isLeaf: 'isLeaf'
})
},
showDelete: {
type: Boolean,
default: false
},
showEdit: {
type: Boolean,
default: false
},
lazy: {
type: Boolean,
default: false
},
isShowHeader: {
type: Boolean,
default: false
},
showCheckbox: {
type: Boolean,
default: false
},
checkStrictly: {
type: Boolean,
default: false
}
})
const applicationYear = ref('')
// 接收父组件传来的事件
const emit = defineEmits([
'nodeClick',
'loadChildren',
'deleteNode',
'nodeExpand',
'nodeCollapse',
'check'
])
// 使用props中的treeProps
const { treeProps } = props
// 添加treeRef
const treeRef = ref(null)
// 展开的节点keys
const expandedKeys = ref([])
// 添加取消选中节点的方法
const clearCurrentNode = () => {
if (treeRef.value) {
treeRef.value.setCurrentKey(null)
}
}
// 设置当前选中的节点
const setCurrentKey = (key) => {
if (treeRef.value) {
treeRef.value.setCurrentKey(key)
}
}
// 设置展开的节点
const setExpandedKeys = (keys) => {
expandedKeys.value = [...keys]
}
// 获取当前展开的节点
const getExpandedKeys = () => {
return expandedKeys.value
}
// 处理复选框选中事件
const handleCheck = (data, { checkedKeys, checkedNodes }) => {
emit('check', {
data,
checkedKeys,
checkedNodes
})
}
// 获取选中的节点
const getCheckedKeys = () => {
return treeRef.value?.getCheckedKeys() || []
}
// 获取半选中的节点
const getHalfCheckedKeys = () => {
return treeRef.value?.getHalfCheckedKeys() || []
}
// 设置选中的节点
const setCheckedKeys = (keys) => {
treeRef.value?.setCheckedKeys(keys)
}
// 暴露方法给父组件
defineExpose({
clearCurrentNode,
setCurrentKey,
setExpandedKeys,
getExpandedKeys,
getCheckedKeys,
getHalfCheckedKeys,
setCheckedKeys
})
const renderContent = (hFn, { node, data }) => {
const content = [
hFn('span', data[props.treeProps.label] || data.label)
]
// 根据showDelete配置决定是否显示删除按钮
if (props.showDelete) {
content.push(
hFn(
'el-button',
{
type: 'danger',
size: 'small',
class: 'delete-btn',
onClick: () => handleDeleteNode(node, data),
},
[
hFn(Delete)
]
)
)
}
// 根据showDelete配置决定是否显示修改按钮
if (props.showEdit) {
content.push(
hFn(
'el-button',
{
type: 'danger',
size: 'small',
class: 'edit-btn',
onClick: () => handleEditNode(data),
},
[
hFn(Edit)
]
)
)
}
return hFn(
'div',
{ class: 'tree-node' },
content
)
}
// 加载子节点数据
const loadNode = (node, resolve) => {
if (!props.lazy) {
return resolve([])
}
if (node.level === 0) {
// 根节点直接返回初始数据
return resolve(props.treeData)
}
// 触发父组件的事件来获取子节点数据
emit('loadChildren', {
node,
resolve: (children) => {
// 确保children是数组
const childNodes = Array.isArray(children) ? children : []
// 将子节点数据设置到当前节点的children属性中
if (node.data) {
node.data.children = childNodes
}
resolve(childNodes)
}
})
}
// 处理节点点击事件
const handleNodeClick = (data, node) => {
emit('nodeClick', data)
}
// 处理删除节点事件
const handleDeleteNode = (node, data) => {
emit('deleteNode', { node, data })
}
// 处理修改节点事件
const handleEditNode = (nodeData) => {
emit('editNode', nodeData)
}
// 处理节点展开
const handleNodeExpand = (data, node) => {
expandedKeys.value = handleNodeExpandUtil({
data,
node,
expandedKeys: expandedKeys.value,
onExpand: (data) => emit('nodeExpand', data)
})
}
// 处理节点收起
const handleNodeCollapse = (data, node) => {
expandedKeys.value = handleNodeCollapseUtil({
data,
expandedKeys: expandedKeys.value,
onCollapse: (data) => emit('nodeCollapse', data)
})
}
</script>
<style scoped>
.tree-container {
height: 100%;
border: 1px solid #e4e7ed;
padding: 10px;
overflow: auto;
}
::v-deep(.tree-node .delete-btn) {
display: none !important;
}
::v-deep(.tree-node .edit-btn) {
display: none !important;
}
::v-deep(.tree-node:hover) {
color: skyblue;
}
::v-deep(.tree-node:hover .delete-btn) {
width: 14px;
display: inline-block !important;
color: red;
margin-left: 5px;
transform: translateY(2px);
}
::v-deep(.tree-node:hover .edit-btn) {
width: 14px;
display: inline-block !important;
color: rgb(17, 0, 255);
margin-left: 5px;
transform: translateY(2px);
}
.tree-header {
border-bottom: 1px solid #e4e7ed;
margin-bottom: 10px;
}
</style>
- treeUtils.js文件
import { nextTick } from 'vue'
/**
* 处理树节点展开
* @param {Object} options 配置选项
* @param {Object} options.data 节点数据
* @param {Object} options.node 节点对象
* @param {Array} options.expandedKeys 展开节点数组
* @param {Function} options.onExpand 展开回调函数
* @returns {Array} 更新后的展开节点数组
*/
export const handleNodeExpand = ({
data,
node,
expandedKeys,
onExpand
}) => {
// 如果节点ID不在展开数组中,则添加
if (!expandedKeys.includes(data.id)) {
expandedKeys.push(data.id)
}
// 确保父节点也保持展开状态
let parent = node.parent
while (parent && parent.data && parent.data.id) {
if (!expandedKeys.includes(parent.data.id)) {
expandedKeys.push(parent.data.id)
}
parent = parent.parent
}
// 调用展开回调
if (onExpand) {
onExpand(data)
}
return expandedKeys
}
/**
* 处理树节点收起
* @param {Object} options 配置选项
* @param {Object} options.data 节点数据
* @param {Array} options.expandedKeys 展开节点数组
* @param {Function} options.onCollapse 收起回调函数
* @returns {Array} 更新后的展开节点数组
*/
export const handleNodeCollapse = ({
data,
expandedKeys,
onCollapse
}) => {
// 从展开数组中移除节点ID
const index = expandedKeys.indexOf(data.id)
if (index > -1) {
expandedKeys.splice(index, 1)
}
// 调用收起回调
if (onCollapse) {
onCollapse(data)
}
return expandedKeys
}
/**
* 处理树节点删除后的展开状态
* @param {Object} options 配置选项
* @param {Object} options.node 要删除的节点
* @param {Object} options.data 节点数据
* @param {Array} options.treeData 树数据
* @param {Function} options.getExpandedKeys 获取展开节点的方法
* @param {Function} options.setExpandedKeys 设置展开节点的方法
* @param {Function} options.clearCurrentNode 清除当前选中节点的方法
* @returns {Promise<void>}
*/
export const handleTreeDelete = async ({
node,
data,
treeData,
getExpandedKeys,
setExpandedKeys,
clearCurrentNode
}) => {
const parent = node.parent
const children = parent.data.children || parent.data
const index = children.findIndex((d) => d.id === data.id)
// 获取当前展开的节点
const currentExpandedKeys = getExpandedKeys()
// 删除节点
children.splice(index, 1)
// 强制刷新treeData
treeData.value = JSON.parse(JSON.stringify(treeData.value))
// 重新设置展开状态
await nextTick()
// 确保父节点保持展开状态
if (parent && parent.data && parent.data.id) {
if (!currentExpandedKeys.includes(parent.data.id)) {
currentExpandedKeys.push(parent.data.id)
}
}
clearCurrentNode()
setExpandedKeys(currentExpandedKeys)
return currentExpandedKeys
}
父组件使用
- 导入组件
import TreeView from '@/components/basicComponents/TreeView'
- 使用组件
<TreeView
ref="treeViewRef"
:treeData="treeData"
:treeProps="customTreeProps"
:showDelete="true"
:lazy="true"
:default-expanded-keys="expandedKeys"
@nodeClick="handleNodeClick"
@deleteNode="handleNodeDelete"
@loadChildren="handleLoadChildren"
@nodeExpand="handleNodeExpand"
@nodeCollapse="handleNodeCollapse"
/>
- 父组件里使用方法
// 定义treeViewRef
const treeViewRef = ref(null)
const treeData = ref([]) //树数据
const expandedKeys = ref([]) // 添加展开节点的key数组
// 自定义树形配置
const customTreeProps = {
children: 'children', // 子节点字段名
label: 'label', // 使用label字段作为显示文本
isLeaf: 'isLeaf' // 是否为叶子节点字段名
}
const handleLoadChildren = async ({ node, resolve }) => {
try {
const children = await fetchTreeChildrenData(node.data.id)
resolve(children)
} catch (error) {
console.error('加载子节点失败:', error)
resolve([]) // 加载失败时返回空数组
}
}
// 获取树子节点数据 懒加载 格式化数据
const fetchTreeChildrenData = async (id = '') => {
const { data } = await getZhuangBeiCategory( id )
const formattedChildren = data.map(item => ({
id: item.id,
label: item.label, // 添加label字段
isLeaf: item.sonNum > 0 ? false : true, // 修正isLeaf的逻辑
children: [] // 初始化为空数组,等待后续加载
}))
if(id) return formattedChildren
treeData.value = formattedChildren
}
//删除子节点
const handleNodeDelete = ({node, data}) => {
ElMessageBox.confirm(
`<div style="text-align: center;">确定要删除【${data.label}】吗?</div>
'提示',
{
dangerouslyUseHTMLString: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async() => {
try{
await deleteZhuangBeiCategory(data.id)
ElMessage({ type: 'success', message: '删除成功!'})
await handleTreeDelete({
node,
data,
treeData,
getExpandedKeys: () => treeViewRef.value.getExpandedKeys(),
setExpandedKeys: (keys) => treeViewRef.value.setExpandedKeys(keys),
clearCurrentNode: () => treeViewRef.value.clearCurrentNode()
})
}catch{
ElMessage({ type: 'error', message: '删除失败!'})
}
}).catch(() => {
// 取消了,不做处理
})
}
// 处理节点展开
const handleNodeExpand = (data) => {
if (!expandedKeys.value.includes(data.id)) {
expandedKeys.value.push(data.id)
}
}
// 处理节点收起
const handleNodeCollapse = (data) => {
const index = expandedKeys.value.indexOf(data.id)
if (index > -1) {
expandedKeys.value.splice(index, 1)
}
}
// 处理节点点击
const handleNodeClick = (nodeData) => {
}
- 其他方法比如复选框,编辑不在示例,感兴趣的可以去试试