Skip to content

Commit 02fa679

Browse files
committed
feat:菜单列表改为虚拟化树形控件实现
1 parent ecfe1b6 commit 02fa679

File tree

1 file changed

+219
-56
lines changed

1 file changed

+219
-56
lines changed

src/views/system/menu/index.vue

Lines changed: 219 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -67,65 +67,87 @@
6767

6868
<!-- 列表 -->
6969
<ContentWrap>
70-
<el-table
70+
<el-tree-v2
7171
v-if="refreshTable"
7272
v-loading="loading"
7373
:data="list"
74-
:default-expand-all="isExpandAll"
75-
row-key="id"
74+
:props="{
75+
label: 'name',
76+
children: 'children'
77+
}"
78+
:default-expanded-keys="isExpandAll ? list.map(item => item.id) : []"
79+
:height="600"
80+
:item-size="40"
81+
:virtual-scroll-horizontal="true"
82+
:highlight-current="true"
83+
@current-change="handleCurrentChange"
7684
>
77-
<el-table-column :show-overflow-tooltip="true" label="菜单名称" prop="name" width="250" />
78-
<el-table-column align="center" label="图标" prop="icon" width="100">
79-
<template #default="scope">
80-
<Icon :icon="scope.row.icon" />
81-
</template>
82-
</el-table-column>
83-
<el-table-column label="排序" prop="sort" width="60" />
84-
<el-table-column :show-overflow-tooltip="true" label="权限标识" prop="permission" />
85-
<el-table-column :show-overflow-tooltip="true" label="组件路径" prop="component" />
86-
<el-table-column :show-overflow-tooltip="true" label="组件名称" prop="componentName" />
87-
<el-table-column label="状态" prop="status">
88-
<template #default="scope">
89-
<el-switch
90-
class="ml-4px"
91-
v-model="scope.row.status"
92-
v-hasPermi="['system:menu:update']"
93-
:active-value="CommonStatusEnum.ENABLE"
94-
:inactive-value="CommonStatusEnum.DISABLE"
95-
:loading="menuStatusUpdating[scope.row.id]"
96-
@change="(val) => handleStatusChanged(scope.row, val as number)"
97-
/>
98-
</template>
99-
</el-table-column>
100-
<el-table-column align="center" label="操作">
101-
<template #default="scope">
102-
<el-button
103-
v-hasPermi="['system:menu:update']"
104-
link
105-
type="primary"
106-
@click="openForm('update', scope.row.id)"
107-
>
108-
修改
109-
</el-button>
110-
<el-button
111-
v-hasPermi="['system:menu:create']"
112-
link
113-
type="primary"
114-
@click="openForm('create', undefined, scope.row.id)"
115-
>
116-
新增
117-
</el-button>
118-
<el-button
119-
v-hasPermi="['system:menu:delete']"
120-
link
121-
type="danger"
122-
@click="handleDelete(scope.row.id)"
123-
>
124-
删除
125-
</el-button>
126-
</template>
127-
</el-table-column>
128-
</el-table>
85+
<template #default="{ data }">
86+
<div
87+
class="custom-tree-node"
88+
:class="{ 'menu-item': true }"
89+
>
90+
<div class="node-content">
91+
<span class="label">{{ data.name }}</span>
92+
<div v-if="currentNode === data" class="menu-info">
93+
<span class="info-item" v-if="data.icon">
94+
<span class="info-label">图标:</span>
95+
<span class="icon-preview">
96+
<Icon :icon="data.icon" />
97+
<span class="icon-name">{{ data.icon }}</span>
98+
</span>
99+
</span>
100+
<span class="info-item">
101+
<span class="info-label">排序:</span>
102+
<span class="info-value">{{ data.sort }}</span>
103+
</span>
104+
<span class="info-item" v-if="data.permission">
105+
<span class="info-label">权限标识:</span>
106+
<span class="info-value">{{ data.permission }}</span>
107+
</span>
108+
<span class="info-item" v-if="data.path">
109+
<span class="info-label">路由地址:</span>
110+
<span class="info-value">{{ data.path }}</span>
111+
</span>
112+
<span class="info-item" v-if="data.component">
113+
<span class="info-label">组件路径:</span>
114+
<span class="info-value">{{ data.component }}</span>
115+
</span>
116+
<span class="info-item" v-if="data.componentName">
117+
<span class="info-label">组件名称:</span>
118+
<span class="info-value">{{ data.componentName }}</span>
119+
</span>
120+
</div>
121+
</div>
122+
<div v-show="currentNode === data" class="operations">
123+
<el-button
124+
v-hasPermi="['system:menu:update']"
125+
link
126+
type="primary"
127+
@click.stop="openForm('update', data.id)"
128+
>
129+
修改
130+
</el-button>
131+
<el-button
132+
v-hasPermi="['system:menu:create']"
133+
link
134+
type="primary"
135+
@click.stop="openForm('create', undefined, data.id)"
136+
>
137+
新增
138+
</el-button>
139+
<el-button
140+
v-hasPermi="['system:menu:delete']"
141+
link
142+
type="danger"
143+
@click.stop="handleDelete(data.id)"
144+
>
145+
删除
146+
</el-button>
147+
</div>
148+
</div>
149+
</template>
150+
</el-tree-v2>
129151
</ContentWrap>
130152

131153
<!-- 表单弹窗:添加/修改 -->
@@ -155,13 +177,26 @@ const queryParams = reactive({
155177
const queryFormRef = ref() // 搜索的表单
156178
const isExpandAll = ref(false) // 是否展开,默认全部折叠
157179
const refreshTable = ref(true) // 重新渲染表格状态
180+
const currentNode = ref<any>(null) // 当前选中节点
158181
159182
/** 查询列表 */
160183
const getList = async () => {
161184
loading.value = true
162185
try {
163186
const data = await MenuApi.getMenuList(queryParams)
164-
list.value = handleTree(data)
187+
// 为每个节点添加 showInfo 属性和样式对象
188+
const addProps = (items: any[]) => {
189+
items.forEach(item => {
190+
item.showInfo = false
191+
item.popupStyle = {}
192+
if (item.children && item.children.length > 0) {
193+
addProps(item.children)
194+
}
195+
})
196+
}
197+
const processedData = handleTree(data)
198+
addProps(processedData)
199+
list.value = processedData
165200
} finally {
166201
loading.value = false
167202
}
@@ -233,8 +268,136 @@ const handleStatusChanged = async (menu: MenuVO, val: number) => {
233268
}
234269
}
235270
271+
const handleCurrentChange = (data: any) => {
272+
currentNode.value = data
273+
// 关闭所有信息面板
274+
list.value.forEach((item: any) => {
275+
item.showInfo = false
276+
})
277+
}
278+
279+
// 添加点击外部关闭弹出层的处理
280+
onMounted(() => {
281+
document.addEventListener('click', (event: MouseEvent) => {
282+
const target = event.target as HTMLElement
283+
if (!target.closest('.menu-info-popup') && !target.closest('.info-button')) {
284+
list.value.forEach((item: any) => {
285+
item.showInfo = false
286+
})
287+
}
288+
})
289+
})
290+
236291
/** 初始化 **/
237292
onMounted(() => {
238293
getList()
239294
})
240295
</script>
296+
297+
<style lang="scss" scoped>
298+
:deep(.el-tree-node.is-current > .el-tree-node__content) {
299+
background-color: var(--el-color-primary-light-7) !important;
300+
301+
.custom-tree-node {
302+
background-color: var(--el-color-primary-light-7);
303+
304+
.operations {
305+
background-color: var(--el-color-primary-light-7);
306+
}
307+
}
308+
}
309+
310+
.custom-tree-node {
311+
flex: 1;
312+
display: flex;
313+
align-items: center;
314+
justify-content: space-between;
315+
padding: 0 8px;
316+
height: 40px;
317+
position: relative;
318+
border-bottom: 1px solid var(--el-border-color-lighter);
319+
min-width: 800px;
320+
transition: background-color 0.3s;
321+
322+
.node-content {
323+
display: flex;
324+
align-items: center;
325+
gap: 12px;
326+
height: 100%;
327+
flex: 1;
328+
min-width: 0;
329+
330+
.label {
331+
flex-shrink: 0;
332+
}
333+
334+
.menu-info {
335+
display: flex;
336+
align-items: center;
337+
gap: 16px;
338+
overflow-x: auto;
339+
flex: 1;
340+
margin-right: 16px;
341+
padding: 0 4px;
342+
343+
&::-webkit-scrollbar {
344+
height: 6px;
345+
}
346+
347+
&::-webkit-scrollbar-thumb {
348+
background: var(--el-border-color);
349+
border-radius: 3px;
350+
}
351+
352+
&::-webkit-scrollbar-track {
353+
background: transparent;
354+
}
355+
}
356+
357+
.info-item {
358+
flex-shrink: 0;
359+
display: flex;
360+
align-items: center;
361+
gap: 4px;
362+
363+
.info-label {
364+
color: var(--el-text-color-secondary);
365+
font-size: 13px;
366+
}
367+
368+
.info-value {
369+
color: var(--el-text-color-primary);
370+
font-size: 13px;
371+
}
372+
373+
.icon-preview {
374+
display: flex;
375+
align-items: center;
376+
gap: 4px;
377+
padding: 0 8px;
378+
height: 24px;
379+
border-radius: 4px;
380+
border: 1px solid var(--el-border-color-lighter);
381+
background-color: var(--el-bg-color);
382+
383+
.icon-name {
384+
font-size: 13px;
385+
color: var(--el-text-color-regular);
386+
}
387+
}
388+
}
389+
}
390+
391+
.operations {
392+
display: flex;
393+
gap: 8px;
394+
height: 100%;
395+
align-items: center;
396+
flex-shrink: 0;
397+
position: sticky;
398+
right: 8px;
399+
padding-left: 8px;
400+
transition: background-color 0.3s;
401+
}
402+
}
403+
</style>

0 commit comments

Comments
 (0)