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

打印 上一主题 下一主题

主题 1728|帖子 1728|积分 5184

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

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


  • TreeView组件
  1. <template>
  2.   <div class="tree-container">
  3.     <div v-if="isShowHeader" class="tree-header">
  4.       <slot name="header"></slot>
  5.     </div>
  6.     <el-tree
  7.       ref="treeRef"
  8.       :data="treeData"
  9.       :props="treeProps"
  10.       highlight-current
  11.       node-key="id"
  12.       :render-content="renderContent"
  13.       :lazy="lazy" :load="lazy ? loadNode : undefined"
  14.       :default-expanded-keys="expandedKeys"
  15.       :show-checkbox="showCheckbox"
  16.       :check-strictly="checkStrictly"
  17.       @node-click="handleNodeClick"
  18.       @node-expand="handleNodeExpand"
  19.       @node-collapse="handleNodeCollapse"
  20.       @check="handleCheck" />
  21.   </div>
  22. </template>
  23. <script setup>
  24. import { defineProps, defineEmits, ref } from 'vue'
  25. import { Delete, Edit } from '@element-plus/icons-vue'
  26. import { handleNodeExpand as handleNodeExpandUtil, handleNodeCollapse as handleNodeCollapseUtil } from '@/utils/treeUtils'
  27. // 接收父组件传来的数据
  28. const props = defineProps({
  29.   treeData: {
  30.     type: Array,
  31.     required: true,
  32.   },
  33.   treeProps: {
  34.     type: Object,
  35.     default: () => ({
  36.       children: 'children',
  37.       label: 'label',
  38.       isLeaf: 'isLeaf'
  39.     })
  40.   },
  41.   showDelete: {
  42.     type: Boolean,
  43.     default: false
  44.   },
  45.   showEdit: {
  46.     type: Boolean,
  47.     default: false
  48.   },
  49.   lazy: {
  50.     type: Boolean,
  51.     default: false
  52.   },
  53.   isShowHeader: {
  54.     type: Boolean,
  55.     default: false
  56.   },
  57.   showCheckbox: {
  58.     type: Boolean,
  59.     default: false
  60.   },
  61.   checkStrictly: {
  62.     type: Boolean,
  63.     default: false
  64.   }
  65. })
  66. const applicationYear = ref('')
  67. // 接收父组件传来的事件
  68. const emit = defineEmits([
  69.   'nodeClick',
  70.   'loadChildren',
  71.   'deleteNode',
  72.   'nodeExpand',
  73.   'nodeCollapse',
  74.   'check'
  75. ])
  76. // 使用props中的treeProps
  77. const { treeProps } = props
  78. // 添加treeRef
  79. const treeRef = ref(null)
  80. // 展开的节点keys
  81. const expandedKeys = ref([])
  82. // 添加取消选中节点的方法
  83. const clearCurrentNode = () => {
  84.   if (treeRef.value) {
  85.     treeRef.value.setCurrentKey(null)
  86.   }
  87. }
  88. // 设置当前选中的节点
  89. const setCurrentKey = (key) => {
  90.   if (treeRef.value) {
  91.     treeRef.value.setCurrentKey(key)
  92.   }
  93. }
  94. // 设置展开的节点
  95. const setExpandedKeys = (keys) => {
  96.   expandedKeys.value = [...keys]
  97. }
  98. // 获取当前展开的节点
  99. const getExpandedKeys = () => {
  100.   return expandedKeys.value
  101. }
  102. // 处理复选框选中事件
  103. const handleCheck = (data, { checkedKeys, checkedNodes }) => {
  104.   emit('check', {
  105.     data,
  106.     checkedKeys,
  107.     checkedNodes
  108.   })
  109. }
  110. // 获取选中的节点
  111. const getCheckedKeys = () => {
  112.   return treeRef.value?.getCheckedKeys() || []
  113. }
  114. // 获取半选中的节点
  115. const getHalfCheckedKeys = () => {
  116.   return treeRef.value?.getHalfCheckedKeys() || []
  117. }
  118. // 设置选中的节点
  119. const setCheckedKeys = (keys) => {
  120.   treeRef.value?.setCheckedKeys(keys)
  121. }
  122. // 暴露方法给父组件
  123. defineExpose({
  124.   clearCurrentNode,
  125.   setCurrentKey,
  126.   setExpandedKeys,
  127.   getExpandedKeys,
  128.   getCheckedKeys,
  129.   getHalfCheckedKeys,
  130.   setCheckedKeys
  131. })
  132. const renderContent = (hFn, { node, data }) => {
  133.   const content = [
  134.     hFn('span', data[props.treeProps.label] || data.label)
  135.   ]
  136.   // 根据showDelete配置决定是否显示删除按钮
  137.   if (props.showDelete) {
  138.     content.push(
  139.       hFn(
  140.         'el-button',
  141.         {
  142.           type: 'danger',
  143.           size: 'small',
  144.           class: 'delete-btn',
  145.           onClick: () => handleDeleteNode(node, data),
  146.         },
  147.         [
  148.           hFn(Delete)
  149.         ]
  150.       )
  151.     )
  152.   }
  153.   // 根据showDelete配置决定是否显示修改按钮
  154.   if (props.showEdit) {
  155.     content.push(
  156.       hFn(
  157.         'el-button',
  158.         {
  159.           type: 'danger',
  160.           size: 'small',
  161.           class: 'edit-btn',
  162.           onClick: () => handleEditNode(data),
  163.         },
  164.         [
  165.           hFn(Edit)
  166.         ]
  167.       )
  168.     )
  169.   }
  170.   return hFn(
  171.     'div',
  172.     { class: 'tree-node' },
  173.     content
  174.   )
  175. }
  176. // 加载子节点数据
  177. const loadNode = (node, resolve) => {
  178.   if (!props.lazy) {
  179.     return resolve([])
  180.   }
  181.   if (node.level === 0) {
  182.     // 根节点直接返回初始数据
  183.     return resolve(props.treeData)
  184.   }
  185.   // 触发父组件的事件来获取子节点数据
  186.   emit('loadChildren', {
  187.     node,
  188.     resolve: (children) => {
  189.       // 确保children是数组
  190.       const childNodes = Array.isArray(children) ? children : []
  191.       // 将子节点数据设置到当前节点的children属性中
  192.       if (node.data) {
  193.         node.data.children = childNodes
  194.       }
  195.       resolve(childNodes)
  196.     }
  197.   })
  198. }
  199. // 处理节点点击事件
  200. const handleNodeClick = (data, node) => {
  201.   emit('nodeClick', data)
  202. }
  203. // 处理删除节点事件
  204. const handleDeleteNode = (node, data) => {
  205.   emit('deleteNode', { node, data })
  206. }
  207. // 处理修改节点事件
  208. const handleEditNode = (nodeData) => {
  209.   emit('editNode', nodeData)
  210. }
  211. // 处理节点展开
  212. const handleNodeExpand = (data, node) => {
  213.   expandedKeys.value = handleNodeExpandUtil({
  214.     data,
  215.     node,
  216.     expandedKeys: expandedKeys.value,
  217.     onExpand: (data) => emit('nodeExpand', data)
  218.   })
  219. }
  220. // 处理节点收起
  221. const handleNodeCollapse = (data, node) => {
  222.   expandedKeys.value = handleNodeCollapseUtil({
  223.     data,
  224.     expandedKeys: expandedKeys.value,
  225.     onCollapse: (data) => emit('nodeCollapse', data)
  226.   })
  227. }
  228. </script>
  229. <style scoped>
  230. .tree-container {
  231.   height: 100%;
  232.   border: 1px solid #e4e7ed;
  233.   padding: 10px;
  234.   overflow: auto;
  235. }
  236. ::v-deep(.tree-node .delete-btn) {
  237.   display: none !important;
  238. }
  239. ::v-deep(.tree-node .edit-btn) {
  240.   display: none !important;
  241. }
  242. ::v-deep(.tree-node:hover) {
  243.   color: skyblue;
  244. }
  245. ::v-deep(.tree-node:hover .delete-btn) {
  246.   width: 14px;
  247.   display: inline-block !important;
  248.   color: red;
  249.   margin-left: 5px;
  250.   transform: translateY(2px);
  251. }
  252. ::v-deep(.tree-node:hover .edit-btn) {
  253.   width: 14px;
  254.   display: inline-block !important;
  255.   color: rgb(17, 0, 255);
  256.   margin-left: 5px;
  257.   transform: translateY(2px);
  258. }
  259. .tree-header {
  260.   border-bottom: 1px solid #e4e7ed;
  261.   margin-bottom: 10px;
  262. }
  263. </style>
复制代码

  • treeUtils.js文件
  1. import { nextTick } from 'vue'
  2. /**
  3. * 处理树节点展开
  4. * @param {Object} options 配置选项
  5. * @param {Object} options.data 节点数据
  6. * @param {Object} options.node 节点对象
  7. * @param {Array} options.expandedKeys 展开节点数组
  8. * @param {Function} options.onExpand 展开回调函数
  9. * @returns {Array} 更新后的展开节点数组
  10. */
  11. export const handleNodeExpand = ({
  12.     data,
  13.     node,
  14.     expandedKeys,
  15.     onExpand
  16. }) => {
  17.     // 如果节点ID不在展开数组中,则添加
  18.     if (!expandedKeys.includes(data.id)) {
  19.         expandedKeys.push(data.id)
  20.     }
  21.    
  22.     // 确保父节点也保持展开状态
  23.     let parent = node.parent
  24.     while (parent && parent.data && parent.data.id) {
  25.         if (!expandedKeys.includes(parent.data.id)) {
  26.             expandedKeys.push(parent.data.id)
  27.         }
  28.         parent = parent.parent
  29.     }
  30.    
  31.     // 调用展开回调
  32.     if (onExpand) {
  33.         onExpand(data)
  34.     }
  35.    
  36.     return expandedKeys
  37. }
  38. /**
  39. * 处理树节点收起
  40. * @param {Object} options 配置选项
  41. * @param {Object} options.data 节点数据
  42. * @param {Array} options.expandedKeys 展开节点数组
  43. * @param {Function} options.onCollapse 收起回调函数
  44. * @returns {Array} 更新后的展开节点数组
  45. */
  46. export const handleNodeCollapse = ({
  47.     data,
  48.     expandedKeys,
  49.     onCollapse
  50. }) => {
  51.     // 从展开数组中移除节点ID
  52.     const index = expandedKeys.indexOf(data.id)
  53.     if (index > -1) {
  54.         expandedKeys.splice(index, 1)
  55.     }
  56.    
  57.     // 调用收起回调
  58.     if (onCollapse) {
  59.         onCollapse(data)
  60.     }
  61.    
  62.     return expandedKeys
  63. }
  64. /**
  65. * 处理树节点删除后的展开状态
  66. * @param {Object} options 配置选项
  67. * @param {Object} options.node 要删除的节点
  68. * @param {Object} options.data 节点数据
  69. * @param {Array} options.treeData 树数据
  70. * @param {Function} options.getExpandedKeys 获取展开节点的方法
  71. * @param {Function} options.setExpandedKeys 设置展开节点的方法
  72. * @param {Function} options.clearCurrentNode 清除当前选中节点的方法
  73. * @returns {Promise<void>}
  74. */
  75. export const handleTreeDelete = async ({
  76.     node,
  77.     data,
  78.     treeData,
  79.     getExpandedKeys,
  80.     setExpandedKeys,
  81.     clearCurrentNode
  82. }) => {
  83.     const parent = node.parent
  84.     const children = parent.data.children || parent.data
  85.     const index = children.findIndex((d) => d.id === data.id)
  86.    
  87.     // 获取当前展开的节点
  88.     const currentExpandedKeys = getExpandedKeys()
  89.    
  90.     // 删除节点
  91.     children.splice(index, 1)
  92.    
  93.     // 强制刷新treeData
  94.     treeData.value = JSON.parse(JSON.stringify(treeData.value))
  95.    
  96.     // 重新设置展开状态
  97.     await nextTick()
  98.     // 确保父节点保持展开状态
  99.     if (parent && parent.data && parent.data.id) {
  100.         if (!currentExpandedKeys.includes(parent.data.id)) {
  101.             currentExpandedKeys.push(parent.data.id)
  102.         }
  103.     }
  104.     clearCurrentNode()
  105.     setExpandedKeys(currentExpandedKeys)
  106.    
  107.     return currentExpandedKeys
  108. }
复制代码
父组件使用


  • 导入组件
  1. import TreeView from '@/components/basicComponents/TreeView'
复制代码

  • 使用组件
  1. <TreeView
  2.         ref="treeViewRef"
  3.           :treeData="treeData"
  4.           :treeProps="customTreeProps"
  5.           :showDelete="true"
  6.           :lazy="true"
  7.           :default-expanded-keys="expandedKeys"
  8.           @nodeClick="handleNodeClick"
  9.           @deleteNode="handleNodeDelete"
  10.           @loadChildren="handleLoadChildren"
  11.           @nodeExpand="handleNodeExpand"
  12.           @nodeCollapse="handleNodeCollapse"
  13.       />
复制代码

  • 父组件里使用方法
  1. // 定义treeViewRef
  2. const treeViewRef = ref(null)
  3. const treeData = ref([]) //树数据
  4. const expandedKeys = ref([]) // 添加展开节点的key数组
  5. // 自定义树形配置
  6. const customTreeProps = {
  7.   children: 'children',  // 子节点字段名
  8.   label: 'label',        // 使用label字段作为显示文本
  9.   isLeaf: 'isLeaf'       // 是否为叶子节点字段名
  10. }
  11. const handleLoadChildren = async ({ node, resolve }) => {
  12.     try {
  13.         const children = await fetchTreeChildrenData(node.data.id)
  14.         resolve(children)
  15.     } catch (error) {
  16.         console.error('加载子节点失败:', error)
  17.         resolve([]) // 加载失败时返回空数组
  18.     }
  19. }
  20. // 获取树子节点数据 懒加载 格式化数据
  21. const fetchTreeChildrenData = async (id = '') => {
  22.     const { data } = await getZhuangBeiCategory( id )
  23.     const formattedChildren = data.map(item => ({
  24.       id: item.id,
  25.       label: item.label,  // 添加label字段
  26.       isLeaf: item.sonNum > 0 ? false : true,  // 修正isLeaf的逻辑
  27.       children: [] // 初始化为空数组,等待后续加载
  28.     }))
  29.     if(id) return formattedChildren
  30.     treeData.value = formattedChildren
  31. }
  32. //删除子节点
  33. const handleNodeDelete = ({node, data}) => {
  34.     ElMessageBox.confirm(
  35.         `<div style="text-align: center;">确定要删除【${data.label}】吗?</div>
  36.         '提示',
  37.         {
  38.             dangerouslyUseHTMLString: true,
  39.             confirmButtonText: '确定',
  40.             cancelButtonText: '取消',
  41.             type: 'warning',
  42.         }
  43.     ).then(async() => {
  44.         try{
  45.             await deleteZhuangBeiCategory(data.id)
  46.             ElMessage({  type: 'success',  message: '删除成功!'})
  47.             await handleTreeDelete({
  48.                 node,
  49.                 data,
  50.                 treeData,
  51.                 getExpandedKeys: () => treeViewRef.value.getExpandedKeys(),
  52.                 setExpandedKeys: (keys) => treeViewRef.value.setExpandedKeys(keys),
  53.                 clearCurrentNode: () => treeViewRef.value.clearCurrentNode()
  54.             })
  55.         }catch{
  56.             ElMessage({  type: 'error',  message: '删除失败!'})
  57.         }
  58.         
  59.         
  60.     }).catch(() => {
  61.         // 取消了,不做处理
  62.     })
  63. }
  64. // 处理节点展开
  65. const handleNodeExpand = (data) => {
  66.   if (!expandedKeys.value.includes(data.id)) {
  67.     expandedKeys.value.push(data.id)
  68.   }
  69. }
  70. // 处理节点收起
  71. const handleNodeCollapse = (data) => {
  72.   const index = expandedKeys.value.indexOf(data.id)
  73.   if (index > -1) {
  74.     expandedKeys.value.splice(index, 1)
  75.   }
  76. }
  77. // 处理节点点击
  78. const handleNodeClick = (nodeData) => {
  79. }
复制代码


  • 其他方法好比复选框,编辑不在示例,感爱好的可以去试试

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

水军大提督

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表