mirror of
https://gitee.com/hhyykk/ipms-sjy-ui.git
synced 2025-02-01 19:24:58 +08:00
【功能优化】菜单管理:使用 el-table-v2 解决菜单过多后,存在卡顿的问题
This commit is contained in:
parent
ea133da1d8
commit
ce60f630c4
@ -5,18 +5,10 @@ const { t } = useI18n() // 国际化
|
||||
|
||||
export function hasPermi(app: App<Element>) {
|
||||
app.directive('hasPermi', (el, binding) => {
|
||||
const { wsCache } = useCache()
|
||||
const { value } = binding
|
||||
const all_permission = '*:*:*'
|
||||
const userInfo = wsCache.get(CACHE_KEY.USER)
|
||||
const permissions = userInfo?.permissions || []
|
||||
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const permissionFlag = value
|
||||
|
||||
const hasPermissions = permissions.some((permission: string) => {
|
||||
return all_permission === permission || permissionFlag.includes(permission)
|
||||
})
|
||||
const hasPermissions = hasPermission(value)
|
||||
|
||||
if (!hasPermissions) {
|
||||
el.parentNode && el.parentNode.removeChild(el)
|
||||
@ -26,3 +18,14 @@ export function hasPermi(app: App<Element>) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const hasPermission = (permission: string[]) => {
|
||||
const { wsCache } = useCache()
|
||||
const all_permission = '*:*:*'
|
||||
const userInfo = wsCache.get(CACHE_KEY.USER)
|
||||
const permissions = userInfo?.permissions || []
|
||||
|
||||
return permissions.some((p: string) => {
|
||||
return all_permission === p || permission.includes(p)
|
||||
})
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
<template #default="{ height, width }">
|
||||
<!-- Virtualized Table 虚拟化表格:高性能,解决表格在大数据量下的卡顿问题 -->
|
||||
<el-table-v2
|
||||
v-loading="loading"
|
||||
:columns="columns"
|
||||
:data="list"
|
||||
:width="width"
|
||||
@ -31,7 +32,7 @@
|
||||
<AreaForm ref="formRef" />
|
||||
</template>
|
||||
<script setup lang="tsx">
|
||||
import type { Column } from 'element-plus'
|
||||
import { Column } from 'element-plus'
|
||||
import AreaForm from './AreaForm.vue'
|
||||
import * as AreaApi from '@/api/system/area'
|
||||
|
||||
@ -40,7 +41,7 @@ defineOptions({ name: 'SystemArea' })
|
||||
// 表格的 column 字段
|
||||
const columns: Column[] = [
|
||||
{
|
||||
dataKey: 'id', // 需要渲染当前列的数据字段。例如说:{id:9527, name:'Mike'},则填 id
|
||||
dataKey: 'id', // 需要渲染当前列的数据字段
|
||||
title: '编号', // 显示在单元格表头的文本
|
||||
width: 400, // 当前列的宽度,必须设置
|
||||
fixed: true, // 是否固定列
|
||||
@ -52,14 +53,17 @@ const columns: Column[] = [
|
||||
width: 200
|
||||
}
|
||||
]
|
||||
// 表格的数据
|
||||
const list = ref([])
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref([]) // 表格的数据
|
||||
|
||||
/**
|
||||
* 获得数据列表
|
||||
*/
|
||||
/** 获得数据列表 */
|
||||
const getList = async () => {
|
||||
list.value = await AreaApi.getAreaTree()
|
||||
loading.value = true
|
||||
try {
|
||||
list.value = await AreaApi.getAreaTree()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
|
@ -67,87 +67,23 @@
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-tree-v2
|
||||
v-if="refreshTable"
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:props="{
|
||||
label: 'name',
|
||||
children: 'children'
|
||||
}"
|
||||
:default-expanded-keys="isExpandAll ? list.map(item => item.id) : []"
|
||||
:height="600"
|
||||
:item-size="40"
|
||||
:virtual-scroll-horizontal="true"
|
||||
:highlight-current="true"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div
|
||||
class="custom-tree-node"
|
||||
:class="{ 'menu-item': true }"
|
||||
>
|
||||
<div class="node-content">
|
||||
<span class="label">{{ data.name }}</span>
|
||||
<div v-if="currentNode === data" class="menu-info">
|
||||
<span class="info-item" v-if="data.icon">
|
||||
<span class="info-label">图标:</span>
|
||||
<span class="icon-preview">
|
||||
<Icon :icon="data.icon" />
|
||||
<span class="icon-name">{{ data.icon }}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="info-item">
|
||||
<span class="info-label">排序:</span>
|
||||
<span class="info-value">{{ data.sort }}</span>
|
||||
</span>
|
||||
<span class="info-item" v-if="data.permission">
|
||||
<span class="info-label">权限标识:</span>
|
||||
<span class="info-value">{{ data.permission }}</span>
|
||||
</span>
|
||||
<span class="info-item" v-if="data.path">
|
||||
<span class="info-label">路由地址:</span>
|
||||
<span class="info-value">{{ data.path }}</span>
|
||||
</span>
|
||||
<span class="info-item" v-if="data.component">
|
||||
<span class="info-label">组件路径:</span>
|
||||
<span class="info-value">{{ data.component }}</span>
|
||||
</span>
|
||||
<span class="info-item" v-if="data.componentName">
|
||||
<span class="info-label">组件名称:</span>
|
||||
<span class="info-value">{{ data.componentName }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="currentNode === data" class="operations">
|
||||
<el-button
|
||||
v-hasPermi="['system:menu:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click.stop="openForm('update', data.id)"
|
||||
>
|
||||
修改
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['system:menu:create']"
|
||||
link
|
||||
type="primary"
|
||||
@click.stop="openForm('create', undefined, data.id)"
|
||||
>
|
||||
新增
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['system:menu:delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click.stop="handleDelete(data.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree-v2>
|
||||
<div style="width: 100%; height: 700px">
|
||||
<!-- AutoResizer 自动调节大小 -->
|
||||
<el-auto-resizer>
|
||||
<template #default="{ height, width }">
|
||||
<!-- Virtualized Table 虚拟化表格:高性能,解决表格在大数据量下的卡顿问题 -->
|
||||
<el-table-v2
|
||||
v-loading="loading"
|
||||
:columns="columns"
|
||||
:data="list"
|
||||
:width="width"
|
||||
:height="height"
|
||||
expand-column-key="name"
|
||||
:default-expanded-keys="isExpandAll ? list.map((item) => item.name) : []"
|
||||
/>
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
@ -160,6 +96,10 @@ import * as MenuApi from '@/api/system/menu'
|
||||
import { MenuVO } from '@/api/system/menu'
|
||||
import MenuForm from './MenuForm.vue'
|
||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
||||
import { h } from 'vue'
|
||||
import { Column, ElButton } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { hasPermission } from '@/directives/permission/hasPermi'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
defineOptions({ name: 'SystemMenu' })
|
||||
@ -168,6 +108,101 @@ const { wsCache } = useCache()
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 表格的 column 字段
|
||||
const columns: Column[] = [
|
||||
{
|
||||
dataKey: 'name',
|
||||
title: '菜单名称',
|
||||
width: 250
|
||||
},
|
||||
{
|
||||
dataKey: 'icon',
|
||||
title: '图标',
|
||||
width: 150,
|
||||
cellRenderer: ({ rowData }) => {
|
||||
return h(Icon, {
|
||||
icon: rowData.icon
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
dataKey: 'sort',
|
||||
title: '排序',
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
dataKey: 'permission',
|
||||
title: '权限标识',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
dataKey: 'component',
|
||||
title: '组件路径',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
dataKey: 'componentName',
|
||||
title: '组件名称',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
dataKey: 'status',
|
||||
title: '状态',
|
||||
width: 120,
|
||||
cellRenderer: ({ rowData }) => {
|
||||
return h(ElSwitch, {
|
||||
modelValue: rowData.status,
|
||||
activeValue: CommonStatusEnum.ENABLE,
|
||||
inactiveValue: CommonStatusEnum.DISABLE,
|
||||
loading: menuStatusUpdating.value[rowData.id],
|
||||
disabled: !hasPermission(['system:menu:update']),
|
||||
onChange: (val) => handleStatusChanged(rowData, val as number)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
dataKey: 'operation',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
cellRenderer: ({ rowData }) => {
|
||||
return h(
|
||||
'div',
|
||||
[
|
||||
hasPermission(['system:menu:update']) &&
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
link: true,
|
||||
type: 'primary',
|
||||
onClick: () => openForm('update', rowData.id)
|
||||
},
|
||||
'修改'
|
||||
),
|
||||
hasPermission(['system:menu:create']) &&
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
link: true,
|
||||
type: 'primary',
|
||||
onClick: () => openForm('create', undefined, rowData.id)
|
||||
},
|
||||
'新增'
|
||||
),
|
||||
hasPermission(['system:menu:delete']) &&
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
link: true,
|
||||
type: 'danger',
|
||||
onClick: () => handleDelete(rowData.id)
|
||||
},
|
||||
'删除'
|
||||
)
|
||||
].filter(Boolean)
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<any>([]) // 列表的数据
|
||||
const queryParams = reactive({
|
||||
@ -176,27 +211,13 @@ const queryParams = reactive({
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const isExpandAll = ref(false) // 是否展开,默认全部折叠
|
||||
const refreshTable = ref(true) // 重新渲染表格状态
|
||||
const currentNode = ref<any>(null) // 当前选中节点
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await MenuApi.getMenuList(queryParams)
|
||||
// 为每个节点添加 showInfo 属性和样式对象
|
||||
const addProps = (items: any[]) => {
|
||||
items.forEach(item => {
|
||||
item.showInfo = false
|
||||
item.popupStyle = {}
|
||||
if (item.children && item.children.length > 0) {
|
||||
addProps(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
const processedData = handleTree(data)
|
||||
addProps(processedData)
|
||||
list.value = processedData
|
||||
list.value = handleTree(data)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -221,11 +242,7 @@ const openForm = (type: string, id?: number, parentId?: number) => {
|
||||
|
||||
/** 展开/折叠操作 */
|
||||
const toggleExpandAll = () => {
|
||||
refreshTable.value = false
|
||||
isExpandAll.value = !isExpandAll.value
|
||||
nextTick(() => {
|
||||
refreshTable.value = true
|
||||
})
|
||||
}
|
||||
|
||||
/** 刷新菜单缓存按钮操作 */
|
||||
@ -268,136 +285,8 @@ const handleStatusChanged = async (menu: MenuVO, val: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleCurrentChange = (data: any) => {
|
||||
currentNode.value = data
|
||||
// 关闭所有信息面板
|
||||
list.value.forEach((item: any) => {
|
||||
item.showInfo = false
|
||||
})
|
||||
}
|
||||
|
||||
// 添加点击外部关闭弹出层的处理
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement
|
||||
if (!target.closest('.menu-info-popup') && !target.closest('.info-button')) {
|
||||
list.value.forEach((item: any) => {
|
||||
item.showInfo = false
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tree-node.is-current > .el-tree-node__content) {
|
||||
background-color: var(--el-color-primary-light-7) !important;
|
||||
|
||||
.custom-tree-node {
|
||||
background-color: var(--el-color-primary-light-7);
|
||||
|
||||
.operations {
|
||||
background-color: var(--el-color-primary-light-7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-tree-node {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
min-width: 800px;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
overflow-x: auto;
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
padding: 0 4px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--el-border-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.info-item {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.info-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.icon-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 8px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
background-color: var(--el-bg-color);
|
||||
|
||||
.icon-name {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operations {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
right: 8px;
|
||||
padding-left: 8px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue
Block a user