|
| 1 | +<script lang="tsx" setup> |
| 2 | +import { defineProps, defineEmits, ref, CSSProperties } from 'vue' |
| 3 | +import { ElTree } from 'element-plus' |
| 4 | +
|
| 5 | +interface TreeProps { |
| 6 | + data: any[] |
| 7 | + treeProps?: Record<string, any> |
| 8 | + width?: string |
| 9 | + height?: string |
| 10 | +} |
| 11 | +const props = defineProps<TreeProps>() |
| 12 | +
|
| 13 | +const emit = defineEmits<{ |
| 14 | + (e: 'node-click', nodeData: any): void |
| 15 | + (e: 'node-expand', nodeData: any): void |
| 16 | + (e: 'node-collapse', nodeData: any): void |
| 17 | +}>() |
| 18 | +
|
| 19 | +const treeContainer = ref<any>(null) |
| 20 | +const showTreeMenu = ref(false) |
| 21 | +const contextNode = ref<any>(null) |
| 22 | +
|
| 23 | +const menuStyle = ref<any>({}) |
| 24 | +
|
| 25 | +const defaultWidth = '300px' |
| 26 | +const defaultHeight = '400px' |
| 27 | +
|
| 28 | +// 关闭菜单 |
| 29 | +const closeTreeMenu = () => { |
| 30 | + showTreeMenu.value = false |
| 31 | + document.removeEventListener('click', closeTreeMenu) |
| 32 | + document.removeEventListener('contextmenu', closeTreeMenu) |
| 33 | +} |
| 34 | +
|
| 35 | +// 右键菜单事件处理函数 |
| 36 | +const openTreeMenu = (event: MouseEvent, data: any, _node: any, _target: HTMLElement) => { |
| 37 | + contextNode.value = data |
| 38 | + if (!treeContainer.value) return |
| 39 | +
|
| 40 | + const containerRect = treeContainer.value.getBoundingClientRect() |
| 41 | + const nodeRect = (event.target as HTMLElement).getBoundingClientRect() |
| 42 | +
|
| 43 | + // 计算菜单相对于父容器定位的坐标 |
| 44 | + const top = nodeRect.top - containerRect.top + treeContainer.value.scrollTop |
| 45 | + const left = nodeRect.left - containerRect.left + treeContainer.value.scrollLeft |
| 46 | +
|
| 47 | + menuStyle.value = { |
| 48 | + position: 'absolute', |
| 49 | + top: `${top + 20}px`, |
| 50 | + left: `${left + 20}px` |
| 51 | + } |
| 52 | +
|
| 53 | + showTreeMenu.value = true |
| 54 | +
|
| 55 | + // 点击其他地方或再次右键关闭菜单 |
| 56 | + document.addEventListener('click', closeTreeMenu) |
| 57 | + document.addEventListener('contextmenu', closeTreeMenu) |
| 58 | +} |
| 59 | +
|
| 60 | +// 节点点击事件 |
| 61 | +const handleNodeClick = (data: any) => { |
| 62 | + emit('node-click', data) |
| 63 | + closeTreeMenu() |
| 64 | +} |
| 65 | +
|
| 66 | +// 节点展开事件 |
| 67 | +const handleNodeExpand = (data: any) => { |
| 68 | + emit('node-expand', data) |
| 69 | + closeTreeMenu() |
| 70 | +} |
| 71 | +
|
| 72 | +// 节点关闭事件 |
| 73 | +const handleNodeCollapse = (data: any) => { |
| 74 | + emit('node-collapse', data) |
| 75 | + closeTreeMenu() |
| 76 | +} |
| 77 | +
|
| 78 | +// 计算容器样式 |
| 79 | +const containerStyle: CSSProperties = { |
| 80 | + position: 'relative', |
| 81 | + overflow: 'auto', |
| 82 | + width: props.width ?? defaultWidth, |
| 83 | + height: props.height ?? defaultHeight |
| 84 | +} |
| 85 | +</script> |
| 86 | +<template> |
| 87 | + <div class="tree-container" ref="treeContainer" :style="containerStyle"> |
| 88 | + <ElTree |
| 89 | + v-bind="treeProps" |
| 90 | + :data="data" |
| 91 | + @node-click="handleNodeClick" |
| 92 | + @node-expand="handleNodeExpand" |
| 93 | + @node-collapse="handleNodeCollapse" |
| 94 | + @node-contextmenu="openTreeMenu" |
| 95 | + > |
| 96 | + <template #default="{ node }"> |
| 97 | + <!-- 如果使用者提供了 render-node slot,则渲染使用者的内容 --> |
| 98 | + <template v-if="$slots['render-node']"> |
| 99 | + <slot name="render-node" :node="node"></slot> |
| 100 | + </template> |
| 101 | + <!-- 否则使用默认节点显示(比如使用 node.label )--> |
| 102 | + <template v-else> |
| 103 | + <span>{{ node.label }}</span> |
| 104 | + </template> |
| 105 | + </template> |
| 106 | + </ElTree> |
| 107 | + <div class="treeMenu" v-show="showTreeMenu" :style="menuStyle"> |
| 108 | + <!-- 用户通过 context-menu slot 来自定义菜单内容 --> |
| 109 | + <slot name="context-menu" :node="contextNode" :data="contextNode"> |
| 110 | + <!-- 如果用户不提供 context-menu slot,可给一个默认内容 --> |
| 111 | + <div style="padding: 8px">No menu defined</div> |
| 112 | + </slot> |
| 113 | + </div> |
| 114 | + <slot></slot> |
| 115 | + </div> |
| 116 | +</template> |
| 117 | +<style scoped lang="less"> |
| 118 | +.treeMenu { |
| 119 | + position: absolute; |
| 120 | + padding: 5px; |
| 121 | + font-size: 14px; |
| 122 | + color: #606266; |
| 123 | + background-color: rgb(255 255 255 / 90%); |
| 124 | + border: 1px solid #dcdcdc; |
| 125 | + border-radius: 5px; |
| 126 | + box-shadow: 0 4px 10px rgb(0 0 0 / 40%); |
| 127 | +
|
| 128 | + /* 移除 overflow: hidden; 或尝试不使用负的 top 值 */ |
| 129 | +
|
| 130 | + /* overflow: hidden; */ |
| 131 | +
|
| 132 | + &::after { |
| 133 | + position: absolute; |
| 134 | +
|
| 135 | + /* 将箭头向上移动到菜单外部 */ |
| 136 | + top: -6px; |
| 137 | + left: 50%; |
| 138 | + border-right: 6px solid transparent; |
| 139 | + border-bottom: 6px solid rgb(206 194 194); |
| 140 | +
|
| 141 | + /* 创建一个向上的箭头 */ |
| 142 | + border-left: 6px solid transparent; |
| 143 | + content: ''; |
| 144 | + transform: translateX(-50%); |
| 145 | + } |
| 146 | +} |
| 147 | +</style> |
0 commit comments