154 lines
5.1 KiB
Vue
154 lines
5.1 KiB
Vue
<template>
|
||
<div class="mj-standard-menu">
|
||
<el-menu
|
||
:default-active="activeIndex"
|
||
:active-text-color="mode === 'horizontal' ? '#409EFF' : undefined"
|
||
class="mj-menu"
|
||
:mode="mode"
|
||
:collapse="isCollapse"
|
||
@select="handleMenuSelect"
|
||
router
|
||
>
|
||
<template v-for="item in menuList" :key="`${item.path}-${item.meta?.title}`">
|
||
<el-sub-menu v-if="item.children && item.children.length > 0">
|
||
<template #title>
|
||
<el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon>
|
||
<span>{{ item.meta.title }}</span>
|
||
</template>
|
||
<el-menu-item :index="resolvePath(item.path, row.path)" v-for="(row,key) in item.children" :key="resolvePath(item.path, row.path)">
|
||
<el-icon v-if="row.meta?.icon"><component :is="row.meta.icon" /></el-icon>
|
||
<template #title>{{ row.meta.title }}</template>
|
||
</el-menu-item>
|
||
</el-sub-menu>
|
||
<el-menu-item v-else :index="getFirstChildPath(item)">
|
||
<el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon>
|
||
<template #title>{{ item.meta.title }}</template>
|
||
</el-menu-item>
|
||
</template>
|
||
</el-menu>
|
||
</div>
|
||
</template>
|
||
<script setup lang="ts">
|
||
defineOptions({ name: "standardMenu" })
|
||
const route = useRoute();
|
||
|
||
const {mode="vertical",menuList,isCollapse,activeMenu} = defineProps<{
|
||
mode?: 'vertical' | 'horizontal'
|
||
menuList: any[]
|
||
isCollapse?:boolean
|
||
activeMenu?: string
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'menu-select', path: string): void
|
||
}>()
|
||
|
||
const activeIndex = computed(() => {
|
||
// 如果传入了 activeMenu,使用它;否则使用默认值
|
||
return activeMenu || route.path
|
||
})
|
||
|
||
/**
|
||
* 智能拼接路径
|
||
* @param parentPath 父级路径
|
||
* @param childPath 子级路径
|
||
*/
|
||
const resolvePath = (parentPath: string, childPath: string) => {
|
||
// 1. 如果子路径是绝对路径(以 / 开头),直接返回子路径
|
||
if (childPath.startsWith('/')) {
|
||
return childPath;
|
||
}
|
||
|
||
// 2. 移除父路径末尾的斜杠
|
||
const parent = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath;
|
||
|
||
// 3. 移除子路径开头的斜杠(虽然第一步已经过滤了,但为了健壮性保留)
|
||
const child = childPath.startsWith('/') ? childPath.slice(1) : childPath;
|
||
|
||
// 4. 返回拼接后的路径
|
||
return `${parent}/${child}`;
|
||
};
|
||
/**
|
||
* 获取菜单项的跳转路径
|
||
* 如果是顶级菜单且有子项,点击应跳转到第一个子项
|
||
*/
|
||
const getFirstChildPath = (item: any) => {
|
||
// 如果是顶级菜单且有子菜单
|
||
if (item.children && item.children.length > 0) {
|
||
return resolvePath(item.path, item.children[0].path);
|
||
}
|
||
// 如果没有子菜单,直接返回 item.path,但要确保它是以 / 开头
|
||
return item.path.startsWith('/') ? item.path : `/${item.path}`;
|
||
};
|
||
|
||
// 处理菜单选中事件
|
||
const handleMenuSelect = (index: string) => {
|
||
// 如果是水平模式(顶部菜单),触发选中事件
|
||
if (mode === 'horizontal') {
|
||
emit('menu-select', index)
|
||
}
|
||
}
|
||
</script>
|
||
<style lang="scss" scoped>
|
||
.mj-standard-menu {
|
||
height: 100%;
|
||
overflow: hidden;
|
||
|
||
:deep(.el-menu) {
|
||
--el-menu-border-color: transparent;
|
||
border-right: none;
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
// 菜单项和子菜单标题的基础样式
|
||
:deep(.el-menu-item),
|
||
:deep(.el-sub-menu__title) {
|
||
transition: padding 0.3s;
|
||
overflow: hidden;
|
||
position: relative;
|
||
|
||
// 文字容器样式 - 使用 flex 布局避免压缩
|
||
span {
|
||
display: inline-block;
|
||
white-space: nowrap;
|
||
width: auto;
|
||
max-width: 1000px; // 设置一个足够大的值
|
||
overflow: hidden;
|
||
transition: max-width 0.25s, opacity 0.25s, width 0.25s;
|
||
opacity: 1;
|
||
vertical-align: middle;
|
||
}
|
||
}
|
||
|
||
// 收缩时隐藏文字 - 先隐藏文字,再改变宽度,避免压缩感
|
||
:deep(.el-menu--collapse) {
|
||
.el-menu-item span,
|
||
.el-sub-menu__title span {
|
||
max-width: 0;
|
||
width: 0;
|
||
opacity: 0;
|
||
transition: opacity 0.15s, max-width 0.2s 0.1s, width 0.2s 0.1s;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
}
|
||
|
||
// 展开时显示文字 - 先改变宽度,再显示文字,避免压缩感
|
||
:deep(.el-menu:not(.el-menu--collapse)) {
|
||
.el-menu-item span,
|
||
.el-sub-menu__title span {
|
||
max-width: 1000px;
|
||
width: auto;
|
||
opacity: 1;
|
||
transition: max-width 0.3s 0.15s, width 0.3s 0.15s, opacity 0.3s 0.2s;
|
||
}
|
||
}
|
||
|
||
// 确保图标不受影响
|
||
:deep(.el-menu-item .el-icon),
|
||
:deep(.el-sub-menu__title .el-icon) {
|
||
flex-shrink: 0;
|
||
transition: none;
|
||
}
|
||
}
|
||
</style> |