148 lines
4.0 KiB
Vue
148 lines
4.0 KiB
Vue
<script lang="tsx" setup>
|
||
import { defineProps, defineEmits, ref, CSSProperties } from 'vue'
|
||
import { ElTree } from 'element-plus'
|
||
|
||
interface TreeProps {
|
||
data: any[]
|
||
treeProps?: Record<string, any>
|
||
width?: string
|
||
height?: string
|
||
}
|
||
const props = defineProps<TreeProps>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'node-click', nodeData: any): void
|
||
(e: 'node-expand', nodeData: any): void
|
||
(e: 'node-collapse', nodeData: any): void
|
||
}>()
|
||
|
||
const treeContainer = ref<any>(null)
|
||
const showTreeMenu = ref(false)
|
||
const contextNode = ref<any>(null)
|
||
|
||
const menuStyle = ref<any>({})
|
||
|
||
const defaultWidth = '300px'
|
||
const defaultHeight = '400px'
|
||
|
||
// 关闭菜单
|
||
const closeTreeMenu = () => {
|
||
showTreeMenu.value = false
|
||
document.removeEventListener('click', closeTreeMenu)
|
||
document.removeEventListener('contextmenu', closeTreeMenu)
|
||
}
|
||
|
||
// 右键菜单事件处理函数
|
||
const openTreeMenu = (event: MouseEvent, data: any, _node: any, _target: HTMLElement) => {
|
||
contextNode.value = data
|
||
if (!treeContainer.value) return
|
||
|
||
const containerRect = treeContainer.value.getBoundingClientRect()
|
||
const nodeRect = (event.target as HTMLElement).getBoundingClientRect()
|
||
|
||
// 计算菜单相对于父容器定位的坐标
|
||
const top = nodeRect.top - containerRect.top + treeContainer.value.scrollTop
|
||
const left = nodeRect.left - containerRect.left + treeContainer.value.scrollLeft
|
||
|
||
menuStyle.value = {
|
||
position: 'absolute',
|
||
top: `${top + 20}px`,
|
||
left: `${left + 20}px`
|
||
}
|
||
|
||
showTreeMenu.value = true
|
||
|
||
// 点击其他地方或再次右键关闭菜单
|
||
document.addEventListener('click', closeTreeMenu)
|
||
document.addEventListener('contextmenu', closeTreeMenu)
|
||
}
|
||
|
||
// 节点点击事件
|
||
const handleNodeClick = (data: any) => {
|
||
emit('node-click', data)
|
||
closeTreeMenu()
|
||
}
|
||
|
||
// 节点展开事件
|
||
const handleNodeExpand = (data: any) => {
|
||
emit('node-expand', data)
|
||
closeTreeMenu()
|
||
}
|
||
|
||
// 节点关闭事件
|
||
const handleNodeCollapse = (data: any) => {
|
||
emit('node-collapse', data)
|
||
closeTreeMenu()
|
||
}
|
||
|
||
// 计算容器样式
|
||
const containerStyle: CSSProperties = {
|
||
position: 'relative',
|
||
overflow: 'auto',
|
||
width: props.width ?? defaultWidth,
|
||
height: props.height ?? defaultHeight
|
||
}
|
||
</script>
|
||
<template>
|
||
<div class="tree-container" ref="treeContainer" :style="containerStyle">
|
||
<ElTree
|
||
v-bind="treeProps"
|
||
:data="data"
|
||
@node-click="handleNodeClick"
|
||
@node-expand="handleNodeExpand"
|
||
@node-collapse="handleNodeCollapse"
|
||
@node-contextmenu="openTreeMenu"
|
||
>
|
||
<template #default="{ node }">
|
||
<!-- 如果使用者提供了 render-node slot,则渲染使用者的内容 -->
|
||
<template v-if="$slots['render-node']">
|
||
<slot name="render-node" :node="node"></slot>
|
||
</template>
|
||
<!-- 否则使用默认节点显示(比如使用 node.label )-->
|
||
<template v-else>
|
||
<span>{{ node.label }}</span>
|
||
</template>
|
||
</template>
|
||
</ElTree>
|
||
<div class="treeMenu" v-show="showTreeMenu" :style="menuStyle">
|
||
<!-- 用户通过 context-menu slot 来自定义菜单内容 -->
|
||
<slot name="context-menu" :node="contextNode" :data="contextNode">
|
||
<!-- 如果用户不提供 context-menu slot,可给一个默认内容 -->
|
||
<div style="padding: 8px">No menu defined</div>
|
||
</slot>
|
||
</div>
|
||
<slot></slot>
|
||
</div>
|
||
</template>
|
||
<style scoped lang="less">
|
||
.treeMenu {
|
||
position: absolute;
|
||
padding: 5px;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
background-color: rgb(255 255 255 / 90%);
|
||
border: 1px solid #dcdcdc;
|
||
border-radius: 5px;
|
||
box-shadow: 0 4px 10px rgb(0 0 0 / 40%);
|
||
|
||
/* 移除 overflow: hidden; 或尝试不使用负的 top 值 */
|
||
|
||
/* overflow: hidden; */
|
||
|
||
&::after {
|
||
position: absolute;
|
||
|
||
/* 将箭头向上移动到菜单外部 */
|
||
top: -6px;
|
||
left: 50%;
|
||
border-right: 6px solid transparent;
|
||
border-bottom: 6px solid rgb(206 194 194);
|
||
|
||
/* 创建一个向上的箭头 */
|
||
border-left: 6px solid transparent;
|
||
content: '';
|
||
transform: translateX(-50%);
|
||
}
|
||
}
|
||
</style>
|