Skip to content

Commit 5a00171

Browse files
committed
feat: 新增支持右键自定义菜单进行节点编辑的树形组件. #569
1 parent 9af5c99 commit 5a00171

File tree

7 files changed

+435
-3
lines changed

7 files changed

+435
-3
lines changed

.vscode/settings.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,16 @@
1515
"i18n-ally.sourceLanguage": "en",
1616
"i18n-ally.displayLanguage": "zh-CN",
1717
"i18n-ally.enabledFrameworks": ["vue", "react"],
18-
"god.tsconfig": "./tsconfig.json"
18+
"god.tsconfig": "./tsconfig.json",
19+
"editor.gotoLocation.alternativeDeclarationCommand": "editor.action.revealDefinition",
20+
"editor.gotoLocation.alternativeDefinitionCommand": "editor.action.revealDefinition",
21+
"editor.gotoLocation.alternativeTypeDefinitionCommand": "editor.action.revealDefinition",
22+
"editor.selectionHighlight": false,
23+
"files.autoSave": "onFocusChange",
24+
"editor.suggest.snippetsPreventQuickSuggestions": false,
25+
"editor.quickSuggestions": {
26+
"other": "on",
27+
"comments": "off",
28+
"strings": "on"
29+
}
1930
}

mock/role/index.mock.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,14 @@ const adminList = [
348348
meta: {
349349
title: 'router.iAgree'
350350
}
351+
},
352+
{
353+
path: 'tree',
354+
component: 'views/Components/Tree',
355+
name: 'Tree',
356+
meta: {
357+
title: 'router.tree'
358+
}
351359
}
352360
]
353361
},

src/components/Tree/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Tree from './src/Tree.vue'
2+
3+
export { Tree }

src/components/Tree/src/Tree.vue

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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>

src/locales/en.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ export default {
190190
personalCenter: 'Personal center',
191191
personal: 'Personal',
192192
avatars: 'Avatars',
193-
iAgree: 'I agree'
193+
iAgree: 'I agree',
194+
tree: 'Tree'
194195
},
195196
permission: {
196197
hasPermission: 'Please set the operation permission value'
@@ -393,6 +394,11 @@ export default {
393394
logoStyle: 'Logo style',
394395
size: 'size config'
395396
},
397+
treeDemo: {
398+
treeTitle: 'Tree control (right-click node to customize menu options)',
399+
message:
400+
'The tree component is based on the secondary packaging of the tree component of ElementPlus'
401+
},
396402
highlightDemo: {
397403
highlight: 'Highlight',
398404
message: 'The best time to plant a tree is ten years ago, followed by now.',

src/locales/zh-CN.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ export default {
186186
personalCenter: '个人中心',
187187
personal: '个人',
188188
avatars: '头像列表',
189-
iAgree: '我同意'
189+
iAgree: '我同意',
190+
tree: 'Tree 树形控件'
190191
},
191192
permission: {
192193
hasPermission: '请设置操作权限值'
@@ -385,6 +386,10 @@ export default {
385386
logoStyle: 'logo样式',
386387
size: '大小配置'
387388
},
389+
treeDemo: {
390+
treeTitle: '树形控件(节点右键可自定义菜单选项)',
391+
message: '基于 ElementPlus 的 Tree 组件二次封装'
392+
},
388393
highlightDemo: {
389394
highlight: '高亮',
390395
message: '种一棵树最好的时间是十年前,其次就是现在。',

0 commit comments

Comments
 (0)