element plus 的 tree 组件虽然是比较好用的,但是并不能满足传统OA系统的对 树 的操作,浏览了整个element plus,Tree 树形控件 嵌套 Dropdown 下拉菜单。当然,如果简单的嵌套,似乎没什么难度,所以我给自己上了点难度,不仅要完美实现效果,还要做到无感刷新。
老规矩,先把 element tree 组件的示例代码搬过来,运行,可以有以下效果
接下来就要实现嵌套,同样需要把 Dropdown 的代码拿过来嵌套
先看效果
上代码,讲解放代码注释里
<template>
<el-input
v-model="filterText"
placeholder="请输入关键字"
class="mb-24"
/>
<el-tree
ref="treeRef"
class="filter-tree"
:data="data"
:props="defaultProps"
:expand-on-click-node="false"
default-expand-all
:filter-node-method="filterNode"
@node-click="handleNodeClick"
>
<template #default="{ node }">
<span>{{ node.label }}</span>
<el-dropdown
class="tree-dropdown"
:style="[activeNode === node.data.$treeNodeId && 'visibility:visible']" // 保证点击的时候能够让操作图标显示
:hide-on-click="false"
placement="right-start"
trigger="click"
>
<span class="inline-block rotate-90">
<el-icon
:size="16"
><Histogram /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleEdit">
重命名
</el-dropdown-item>
<el-dropdown-item>
<el-dropdown
placement="right-start"
>
<span class="flex justify-between">
添加<el-icon
:size="16"
><ArrowRight /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleInsertBefore">
上方添加目录
</el-dropdown-item>
<el-dropdown-item @click="handleInsertAfter">
下方添加目录
</el-dropdown-item>
<el-dropdown-item
:disabled="node.level===3" // 只有第一第二级才能添加子目录
@click="handleInsertChild"
>
添加子目录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-dropdown-item>
<el-dropdown-item>
<el-dropdown
placement="right-start"
>
<span class="flex justify-between">
移动<el-icon size="16"><ArrowRight /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
:disabled="!node.previousSibling" // 第一个不能上移
@click="handleMoveUp(node)"
>
上移
</el-dropdown-item>
<el-dropdown-item
:disabled="!node.nextSibling" // 最后一个不能下移
@click="handleMoveDown(node)"
>
下移
</el-dropdown-item>
<!-- <el-dropdown-item // 升降级,可以做,但是没必要,暂时屏蔽,有需要的要自己实现
:disabled="node.level===3" // 第三级不能降级
@click="handleTierDown(node)"
>
降级
</el-dropdown-item>
<el-dropdown-item
:disabled="node.level===1" // 第一级不能升级
@click="handleUpgrades(node)"
>
升级
</el-dropdown-item> -->
</el-dropdown-menu>
</template>
</el-dropdown>
</el-dropdown-item>
<el-dropdown-item @click="handleClone(node)">
复制
</el-dropdown-item>
<el-dropdown-item @click="handleDelete">
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-tree>
// 弹窗自己写,我的是组件,就不放代码了
</template>
<script lang="ts" setup>
import { toRefs, onMounted, ref, Ref, watch } from 'vue'
import { ElTree, ElMessage, ElMessageBox } from 'element-plus'
import { Histogram, ArrowRight } from '@element-plus/icons'
import { v4 as uuidV4 } from 'uuid'
interface Tree {
[key: string]: any
}
const emits = defineEmits(['changeScale'])
const filterText = ref('')
const treeRef = ref<InstanceType<typeof ElTree>>()
const defaultProps = {
children: 'children',
label: 'label',
}
const name = ref('') // 重命名
const activeNode = ref() // 选中的虚拟树节点id,不是自己定义的
const activeNodeName = ref('') // 选中树的名字,方便重命名/删除
const level = ref() // 选中层级
const action = ref('insertBefore') as Ref<'insertBefore'|'insertAfter'|'append'> // 当前选中的操作,因为下拉菜单是嵌套的,只能够记录当前操作。
const handleInsertBefore = () => {
action.value = 'insertBefore'
// 这里让你的弹窗显示
// 弹窗.title = "添加目录"
}
const handleInsertAfter = () => {
action.value = 'insertAfter'
// 这里让你的弹窗显示
// 弹窗.title = "添加目录"
}
const handleInsertChild = () => {
action.value = 'append'
// 这里让你的弹窗显示
// 弹窗.title = "添加子目录"
}
const handleEdit = () => {
action.value = 'insertAfter'
name.value = activeNodeName.value // 把原来的名字带到弹窗去
// 这里让你的弹窗显示
// 弹窗.title = "修改目录"
}
const handleClone = async (node) => {
try {
await ElMessageBox.confirm(`确定复制该模板吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
center: true
})
const { $treeNodeId, ...newData } = node.data
if (treeRef.value) treeRef.value.insertAfter(newData, $treeNodeId) // 复制的原理就是,在改节点下方复制一个一模一样的,原则来说,包含的id是要替换掉新的,这个就得涉及到结构赋值了。
activeNode.value = '' // 清空选中的节点id,让图标隐藏
} catch (err) {
ElMessage({ message: '已取消', showClose: true, type: 'info' })
return
}
}
const handleDelete = async () => {
try {
await ElMessageBox.confirm(`是否删除 ${activeNodeName.value}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
center: true
})
if (treeRef.value) treeRef.value.remove(activeNode.value)
activeNode.value = '' // 清空选中的节点id,让图标隐藏
} catch (err) {
ElMessage({ message: '已取消', showClose: true, type: 'info' })
return
}
}
const handleMoveUp = (node) => { // 上移的原理就是现在选中节点上方复制一个一模一样的节点,然后删掉原来那个
const { $treeNodeId, ...newData } = node.data
if (treeRef.value) treeRef.value.insertBefore(newData, node.previousSibling.data.$treeNodeId)
if (treeRef.value) treeRef.value.remove($treeNodeId)
}
const handleMoveDown = (node) => { // 下移的原理就是现在选中节点下方复制一个一模一样的节点,然后删掉原来那个
const { $treeNodeId, ...newData } = node.data
if (treeRef.value) treeRef.value.insertAfter(newData, node.nextSibling.data.$treeNodeId)
if (treeRef.value) treeRef.value.remove($treeNodeId)
}
// const handleTierDown = (node) => {
// console.log(node)
// }
// const handleUpgrades = (node) => {
// console.log(node)
// }
watch(filterText, (val) => {
treeRef.value!.filter(val)
})
const filterNode = (value: string, data: Tree) => {
if (!value) return true
return data.label.includes(value)
}
const data: Tree[] = [
{
label: 'Level one 1',
children: [
{
label: 'Level two 1-1',
children: [
{
label: 'Level three 1-1-1',
},
],
},
],
},
{
label: 'Level one 2',
children: [
{
label: 'Level two 2-1',
children: [
{
label: 'Level three 2-1-1',
},
],
},
{
label: 'Level two 2-2',
children: [
{
label: 'Level three 2-2-1',
},
],
},
],
},
{
label: 'Level one 3',
children: [
{
label: 'Level two 3-1',
children: [
{
label: 'Level three 3-1-1',
},
],
},
{
label: 'Level two 3-2',
children: [
{
label: 'Level three 3-2-1',
},
],
},
],
},
]
const handleNodeClick = (event) => {
console.log(event)
activeNode.value = event.$treeNodeId // 选中的节点树id
activeNodeName.value = event.label // 选中的名字
level.value = event.level // 选中的层级
}
const handleSubmit = () => { // 这里是你的弹窗做出正确选择的时候触发
submitBoxProps.value.visible = false
const data: object = {
id: uuidV4(), // 定义新的id,这里用的是uuid,你可以先跑跟后端约定好的接口,让他把对应的 data 返回给你
label: name.value, // 你弹窗输入的内容(节点名字)
}
level.value === 3 ? Object.assign(data, { // 你的第三级自己的内容 }) : Object.assign(data, {children: []}) // 根据层级,重新重构data
if (action.value === 'insertBefore' && treeRef.value) treeRef.value.insertBefore(data, activeNode.value) // 上方插入
if (action.value === 'insertAfter' && treeRef.value) treeRef.value.insertAfter(data, activeNode.value) // 下方插入
if (action.value === 'append' && treeRef.value) treeRef.value.append(data, activeNode.value) // 子插入
activeNode.value = ''
}
</script>
<style lang="scss" scoped>
.template-tree {
width: 300px;
height: max-content;
background: #ffffff;
:deep(.el-tree-node__content) {
.tree-dropdown {
visibility: hidden; // 默认让菜单时隐藏的
}
}
:deep(.el-tree-node__content:active),
:deep(.el-tree-node__content:hover) {
.tree-dropdown {
visibility: visible !important; // 菜单显示
}
}
}
</style>
注意:每一个方法都应该配合后端来实现的,无感刷新的原理是,前端手动更改了树,而不是每掉一次后端接口就刷新获取数据。
希望能帮到您。