<template> <div class="custom-tree-container" @contextmenu.native="handlePaste($event)"> <!-- <el-tree :data="dataSource" show-checkbox node-key="id" default-expand-all :expand-on-click-node="false" :render-content="renderContent" @node-contextmenu="" /> --> <div class="search"> <el-input placeholder="输入关键字进行搜索" v-model="filterText" @keyup.enter.native="searchUp" @blur="searchUp" clearable style="margin-right: 8px;"></el-input> <el-button type="primary" @click="setAllNode"> <el-icon> <Expand v-if="allNode" /> <Fold v-else /> </el-icon> </el-button> </div> <el-tree ref="treeRefs" :data="dataSource" node-key="id" default-expand-all expand-on-click-node @node-contextmenu="nodeContextmenu" :props="{ value: 'id', label: 'name', children: 'children' }" :filter-node-method="filterMethod" draggable @node-drop="dropSuccess"> <template #default="{ node, data }"> <div class="custom-tree-node"> <div v-if="data.id !== reNameId">{{ data.name }}</div> <el-input v-else size="small" v-model="data.name" @blur="inputBlur(data.name)" v-focus /> <!-- <span> //自定义节点内容 <a @click="append(data)"> Add </a> <a style="margin-left: 8px" @click="remove(node, data)"> Delete </a> </span> --> </div> </template> </el-tree> <!-- Dialog --> <el-dialog v-model="removeVisible" title="确认删除" width="30%" :before-close="handleClose"> <span>此操作将删除当前节点及全部子节点,确认是否删除?</span> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="remove(true)"> 确认 </el-button> <el-button @click="remove(false)">取消</el-button> </span> </template> </el-dialog> <!-- Menu --> <div class="menu" v-show="menuState"> <div class="menu-item menu-rename" @click="rename">重命名</div> <div class="menu-item menu-add-peer" @click="addPeer">添加同级节点</div> <div class="menu-item menu-add-sublevel" @click="addSublevel">添加子级节点</div> <div class="menu-item menu-set-property" @click="setProperty">设置属性</div> <div class="menu-item menu-del-node" @click="delNode">删除</div> </div> <div class="blur" @click="unBlur('blur')" v-show="menuState" @contextmenu.native="handlePaste($event)"></div> </div> </template> <script setup> import { getCurrentInstance, ref } from 'vue' const menuState = ref(false) const contextMenuRow = ref(null) //右键存储数据 const parentMenuRow = ref(null) //右键节点父级 const reNameId = ref(null) //重命名记录 const reName = ref(null) //重命名记录 const removeVisible = ref(false) const filterText = ref('') const allNode = ref(false) const treeRefs = ref(null); const { proxy } = getCurrentInstance(); const dataSource = ref([ { id: 1, name: '一级 1', pid: null, children: [ { id: 4, name: '二级 1-1', pid: 1, children: [ { id: 9, name: '三级 1-1-1', pid: 4, }, { id: 10, name: '三级 1-1-2', pid: 4, }, ], }, { id: 36, name: '二级 2-1', pid: 1, children: [ { id: 202, name: '三级 1-2-1', pid: 36, }, { id: 109, name: '三级 1-2-2', pid: 36, }, ], }, ], }, { id: 2, name: '一级 2', pid: null, children: [ { id: 5, name: '二级 2-1', pid: 2, }, { id: 6, name: '二级 2-2', pid: 2, }, ], }, { id: 3, name: '一级 3', pid: null, children: [ { id: 7, name: '二级 3-1', pid: 3, }, { id: 8, name: '二级 3-2', pid: 3, }, ], }, ]) const handlePaste = (event) => { // 禁用鼠标右键 event.preventDefault() //关闭菜单 return false } //设置新数据 const updateKeyChildren = (key, value) => { console.log('为节点设置新数据', key, value) } //右击事件 const nodeContextmenu = (event, data, node) => { console.log('nodeContextmenu', data, node) contextMenuRow.value = data if (data.pid) { parentMenuRow.value = node.parent.data } //show menu let menu = document.querySelector('.menu') menu.style.left = event.x + 8 + 'px' menu.style.top = event.y + 'px' menuState.value = true } const rename = () => { reNameId.value = contextMenuRow.value.id reName.value = contextMenuRow.value.name //关闭菜单 menuState.value = false } const unBlur = (state) => { console.log('取消操作菜单') if (state == 'inputName') { reName.value = null reNameId.value = null contextMenuRow.value = null parentMenuRow.value = null return } if (state == 'addChild') { menuState.value = false contextMenuRow.value = null parentMenuRow.value = null } if (state == 'remove') { menuState.value = false reName.value = null reNameId.value = null contextMenuRow.value = null parentMenuRow.value = null } if(state == 'blur'){ menuState.value = false reName.value = null reNameId.value = null contextMenuRow.value = null parentMenuRow.value = null } } const inputBlur = (name) => { console.log('更新命名', name) if (reName.value !== name) { console.log('调用接口,更新list') } unBlur('inputName') } const addSublevel = () => { const addId = +new Date() const newChild = { id: addId, name: '未命名', children: [] } reName.value = '未命名' reNameId.value = addId if (!contextMenuRow.value.children) { contextMenuRow.value.children = [] } contextMenuRow.value.children.push(newChild) dataSource.value = [...dataSource.value] unBlur('addChild') } const addPeer = () => { const addId = +new Date() const newChild = { id: addId, name: '未命名', children: [] } reName.value = '未命名' reNameId.value = addId //静态添加 if (parentMenuRow.value) { if (!parentMenuRow.value.children) { parentMenuRow.value.children = [] } parentMenuRow.value.children.push(newChild) dataSource.value = [...dataSource.value] } else { console.log('没有父级节点') dataSource.value.push(newChild) } unBlur('addChild') } const delNode = (node, data) => { console.log('删除,弹窗', node, data, removeVisible) removeVisible.value = true } const handleClose = () => { console.log('handleClose') removeVisible.value = false unBlur('remove') } const remove = (bool) => { if (bool) { console.log('调用删除') } removeVisible.value = false unBlur('remove') } // 展开收起所有节点 const setAllNode = () => { //一级展开不关注child let treeList = dataSource.value; for (let i = 0; i < treeList.length; i++) { treeRefs.value.store.nodesMap[treeList[i].id].expanded = allNode.value; } allNode.value = !allNode.value //全部展开,但会出现卡顿 // const nodes = treeRefs.value.store._getAllNodes(); // nodes.forEach(item => { // item.expanded = allNode.value; // }) } watch(filterText, (val) => { proxy.$refs.treeRefs.filter(val); }) const searchUp = () => { if (filterText.value && filterText.value.length) { proxy.$refs.treeRefs.filter(filterText.value); allNode.value = false } else { let treeList = dataSource.value; for (let i = 0; i < treeList.length; i++) { treeRefs.value.store.nodesMap[treeList[i].id].expanded = false; } allNode.value = false } } const filterMethod = (query, data) => { return data.name.includes(query) } const dropSuccess = (node, toItem, position, event) => { console.log('拖拽', node, toItem, position, event) } </script> <style> .custom-tree-node { flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 8px; } .menu { width: 120px; z-index: 999; background-color: #ffffff; position: fixed; text-align: left; box-shadow: 0px 0px 5px 1px rgba(102, 102, 102, 0.35); } .blur { width: 100%; height: 100%; z-index: 99; position: absolute; top: 0; left: 0; } .menu-item { padding-left: 10px; color: #333333; font-size: 13px; line-height: 24px; margin: 8px 0; } .menu-item:hover { background-color: #EBF5FF; } .search { margin-bottom: 10px; display: flex; } </style> //树形结构接口需求 { "id": 4, //节点id "name": "二级 1-1", //节点name "children": [ //子节点数组 { "id": 9, "name": "三级 1-1-1", "children":[], "pid":4 }, { "id": 10, "name": "三级 1-1-2", "children":[], "pid":4 } ], "pid":2, //父级id //setAttribute或attributeList "setAttribute":'TY', //节点身份属性,取决于二级菜单展示内容 "attributeList":[ //节点二级菜单内容 { "id": 129, "name": "属性1", ... }, { "id": 103, "name": "属性2", ... } ] }
复制即用,根据需求自行修改
效果图
<template> <div class="custom-tree-container" @contextmenu.native="handlePaste($event)"> <!-- <el-tree :data="dataSource" show-checkbox node-key="id" default-expand-all :expand-on-click-node="false" :render-content="renderContent" @node-contextmenu="" /> --> <div class="search"> <el-input placeholder="输入关键字进行搜索" v-model="filterText" @keyup.enter.native="searchUp" @blur="searchUp" clearable style="margin-right: 8px;"></el-input> <el-button type="primary" @click="setAllNode"> <el-icon> <Expand v-if="allNode" /> <Fold v-else /> </el-icon> </el-button> </div> <el-tree ref="treeRefs" :data="dataSource" node-key="id" default-expand-all expand-on-click-node @node-contextmenu="nodeContextmenu" :props="{ value: 'id', label: 'name', children: 'children' }" :filter-node-method="filterMethod" draggable @node-drop="dropSuccess"> <template #default="{ node, data }"> <div class="custom-tree-node"> <div v-if="data.id !== reNameId">{{ data.name }}</div> <el-input v-else size="small" v-model="data.name" @blur="inputBlur(data.name)" v-focus /> <!-- <span> //自定义节点内容 <a @click="append(data)"> Add </a> <a style="margin-left: 8px" @click="remove(node, data)"> Delete </a> </span> --> </div> </template> </el-tree> <!-- Dialog --> <el-dialog v-model="removeVisible" title="确认删除" width="30%" :before-close="handleClose"> <span>此操作将删除当前节点及全部子节点,确认是否删除?</span> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="remove(true)"> 确认 </el-button> <el-button @click="remove(false)">取消</el-button> </span> </template> </el-dialog> <!-- Menu --> <div class="menu" v-show="menuState"> <div class="menu-item menu-rename" @click="rename">重命名</div> <div class="menu-item menu-add-peer" @click="addPeer">添加同级节点</div> <div class="menu-item menu-add-sublevel" @click="addSublevel">添加子级节点</div> <div class="menu-item menu-set-property" @click="setProperty">设置属性</div> <div class="menu-item menu-del-node" @click="delNode">删除</div> </div> <div class="blur" @click="unBlur('blur')" v-show="menuState" @contextmenu.native="handlePaste($event)"></div> </div> </template> <script setup> import { getCurrentInstance, ref } from 'vue' const menuState = ref(false) const contextMenuRow = ref(null) //右键存储数据 const parentMenuRow = ref(null) //右键节点父级 const reNameId = ref(null) //重命名记录 const reName = ref(null) //重命名记录 const removeVisible = ref(false) const filterText = ref('') const allNode = ref(false) const treeRefs = ref(null); const { proxy } = getCurrentInstance(); const dataSource = ref([ { id: 1, name: '一级 1', pid: null, children: [ { id: 4, name: '二级 1-1', pid: 1, children: [ { id: 9, name: '三级 1-1-1', pid: 4, }, { id: 10, name: '三级 1-1-2', pid: 4, }, ], }, { id: 36, name: '二级 2-1', pid: 1, children: [ { id: 202, name: '三级 1-2-1', pid: 36, }, { id: 109, name: '三级 1-2-2', pid: 36, }, ], }, ], }, { id: 2, name: '一级 2', pid: null, children: [ { id: 5, name: '二级 2-1', pid: 2, }, { id: 6, name: '二级 2-2', pid: 2, }, ], }, { id: 3, name: '一级 3', pid: null, children: [ { id: 7, name: '二级 3-1', pid: 3, }, { id: 8, name: '二级 3-2', pid: 3, }, ], }, ]) const handlePaste = (event) => { // 禁用鼠标右键 event.preventDefault() //关闭菜单 return false } //设置新数据 const updateKeyChildren = (key, value) => { console.log('为节点设置新数据', key, value) } //右击事件 const nodeContextmenu = (event, data, node) => { console.log('nodeContextmenu', data, node) contextMenuRow.value = data if (data.pid) { parentMenuRow.value = node.parent.data } //show menu let menu = document.querySelector('.menu') menu.style.left = event.x + 8 + 'px' menu.style.top = event.y + 'px' menuState.value = true } const rename = () => { reNameId.value = contextMenuRow.value.id reName.value = contextMenuRow.value.name //关闭菜单 menuState.value = false } const unBlur = (state) => { console.log('取消操作菜单') if (state == 'inputName') { reName.value = null reNameId.value = null contextMenuRow.value = null parentMenuRow.value = null return } if (state == 'addChild') { menuState.value = false contextMenuRow.value = null parentMenuRow.value = null } if (state == 'remove') { menuState.value = false reName.value = null reNameId.value = null contextMenuRow.value = null parentMenuRow.value = null } if(state == 'blur'){ menuState.value = false reName.value = null reNameId.value = null contextMenuRow.value = null parentMenuRow.value = null } } const inputBlur = (name) => { console.log('更新命名', name) if (reName.value !== name) { console.log('调用接口,更新list') } unBlur('inputName') } const addSublevel = () => { const addId = +new Date() const newChild = { id: addId, name: '未命名', children: [] } reName.value = '未命名' reNameId.value = addId if (!contextMenuRow.value.children) { contextMenuRow.value.children = [] } contextMenuRow.value.children.push(newChild) dataSource.value = [...dataSource.value] unBlur('addChild') } const addPeer = () => { const addId = +new Date() const newChild = { id: addId, name: '未命名', children: [] } reName.value = '未命名' reNameId.value = addId //静态添加 if (parentMenuRow.value) { if (!parentMenuRow.value.children) { parentMenuRow.value.children = [] } parentMenuRow.value.children.push(newChild) dataSource.value = [...dataSource.value] } else { console.log('没有父级节点') dataSource.value.push(newChild) } unBlur('addChild') } const delNode = (node, data) => { console.log('删除,弹窗', node, data, removeVisible) removeVisible.value = true } const handleClose = () => { console.log('handleClose') removeVisible.value = false unBlur('remove') } const remove = (bool) => { if (bool) { console.log('调用删除') } removeVisible.value = false unBlur('remove') } // 展开收起所有节点 const setAllNode = () => { //一级展开不关注child let treeList = dataSource.value; for (let i = 0; i < treeList.length; i++) { treeRefs.value.store.nodesMap[treeList[i].id].expanded = allNode.value; } allNode.value = !allNode.value //全部展开,但会出现卡顿 // const nodes = treeRefs.value.store._getAllNodes(); // nodes.forEach(item => { // item.expanded = allNode.value; // }) } watch(filterText, (val) => { proxy.$refs.treeRefs.filter(val); }) const searchUp = () => { if (filterText.value && filterText.value.length) { proxy.$refs.treeRefs.filter(filterText.value); allNode.value = false } else { let treeList = dataSource.value; for (let i = 0; i < treeList.length; i++) { treeRefs.value.store.nodesMap[treeList[i].id].expanded = false; } allNode.value = false } } const filterMethod = (query, data) => { return data.name.includes(query) } const dropSuccess = (node, toItem, position, event) => { console.log('拖拽', node, toItem, position, event) } </script> <style> .custom-tree-node { flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 8px; } .menu { width: 120px; z-index: 999; background-color: #ffffff; position: fixed; text-align: left; box-shadow: 0px 0px 5px 1px rgba(102, 102, 102, 0.35); } .blur { width: 100%; height: 100%; z-index: 99; position: absolute; top: 0; left: 0; } .menu-item { padding-left: 10px; color: #333333; font-size: 13px; line-height: 24px; margin: 8px 0; } .menu-item:hover { background-color: #EBF5FF; } .search { margin-bottom: 10px; display: flex; } </style> //树形结构接口需求 { "id": 4, //节点id "name": "二级 1-1", //节点name "children": [ //子节点数组 { "id": 9, "name": "三级 1-1-1", "children":[], "pid":4 }, { "id": 10, "name": "三级 1-1-2", "children":[], "pid":4 } ], "pid":2, //父级id //setAttribute或attributeList "setAttribute":'TY', //节点身份属性,取决于二级菜单展示内容 "attributeList":[ //节点二级菜单内容 { "id": 129, "name": "属性1", ... }, { "id": 103, "name": "属性2", ... } ] } 标签:const,name,pid,Tree,value,element,右键,null,id From: https://www.cnblogs.com/awench/p/17361022.html