elementplus el-tree 二次封装支持配置删除后展示展开或折叠编辑复选框懒加载功能

发布于:2025-05-15 ⋅ 阅读:(14) ⋅ 点赞:(0)

本文介绍了基于 ElementPlus 的 el-tree 组件进行二次封装的 TreeView 组件,使用 Vue3 和 JavaScript 实现。TreeView 组件通过 props 接收树形数据、配置项等,支持懒加载、节点展开/收起、节点点击、删除、编辑等操作。组件内部通过 ref 管理树实例,并提供了 clearCurrentNode、setCurrentKey、setExpandedKeys 等方法供父组件调用。renderContent 方法用于自定义节点内容,支持根据配置显示删除和编辑按钮。事件处理函数通过 emit 将节点操作传递给父组件,实现了组件与父组件的交互。样式部分通过 scoped 样式隔离,确保组件样式独立。

准备组件 TreeView treeUtils方法

  1. 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>
  1. 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
} 

父组件使用

  1. 导入组件
import TreeView from '@/components/basicComponents/TreeView'
  1. 使用组件
<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"
      />
  1. 父组件里使用方法
// 定义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) => {
}
  • 其他方法比如复选框,编辑不在示例,感兴趣的可以去试试

网站公告

今日签到

点亮在社区的每一天
去签到