Files
schoolNews/schoolNewsWeb/src/components/base/MenuItem.vue
2025-10-28 19:04:35 +08:00

222 lines
4.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="menu-item">
<!-- 有子菜单的情况 -->
<template v-if="hasChildren">
<div
class="menu-item-content"
:class="{ 'active': isActive }"
@click="toggleExpanded"
>
<div class="menu-item-inner">
<img
v-if="menu.icon"
:src="String(PUBLIC_IMG_PATH + '/' + menu.icon)"
class="tab-icon"
:alt="String(menu.name || '')"
/>
<span class="menu-title">{{ menu.name }}</span>
<img
src="@/assets/imgs/arrow-down.svg"
alt="arrow"
class="expand-icon"
:class="{ 'expanded': expanded }"
/>
</div>
</div>
<!-- 子菜单 -->
<transition name="submenu">
<div
class="submenu"
v-if="expanded"
>
<MenuItem
v-for="child in filteredChildren"
:key="child.menuID"
:menu="child"
:level="level + 1"
@menu-click="$emit('menu-click', $event)"
/>
</div>
</transition>
</template>
<!-- 没有子菜单的情况 -->
<template v-else>
<div
class="menu-item-content"
:class="{ 'active': isActive }"
@click="handleClick"
>
<div class="menu-item-inner">
<img
v-if="menu.icon"
:src="String(PUBLIC_IMG_PATH + '/' + menu.icon)"
class="tab-icon"
:alt="String(menu.name || '')"
/>
<span class="menu-title">{{ menu.name }}</span>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import type { SysMenu } from '@/types';
import { MenuType } from '@/types/enums';
import { PUBLIC_IMG_PATH} from '@/config'
// 递归组件需要声明名称Vue 3.5+
defineOptions({
name: 'MenuItem'
});
// Props
interface Props {
menu: SysMenu;
level?: number;
}
const props = withDefaults(defineProps<Props>(), {
level: 0
});
// Emits
const emit = defineEmits<{
'menu-click': [menu: SysMenu];
}>();
// 状态 - 顶层菜单默认展开
const expanded = ref(props.level === 0);
// Composition API
const route = useRoute();
// 计算属性
const hasChildren = computed(() => {
// 只显示SIDEBAR类型的子菜单过滤掉PAGE类型
return props.menu.children &&
props.menu.children.filter((child: SysMenu) => child.type === MenuType.SIDEBAR).length > 0;
});
// 过滤后的子菜单只显示SIDEBAR类型
const filteredChildren = computed(() => {
if (!props.menu.children) return [];
return props.menu.children.filter((child: SysMenu) => child.type === MenuType.SIDEBAR);
});
const isActive = computed(() => {
// 检查当前路由是否匹配此菜单
return route.path === props.menu.url;
});
// 方法 - 使用 function 声明
function toggleExpanded() {
if (hasChildren.value) {
expanded.value = !expanded.value;
}
}
function handleClick() {
// 支持NAVIGATION和SIDEBAR类型的菜单点击
if (props.menu.url && (props.menu.type === MenuType.NAVIGATION || props.menu.type === MenuType.SIDEBAR)) {
emit('menu-click', props.menu);
}
}
</script>
<style lang="scss" scoped>
.menu-item {
margin-bottom: 4px;
}
.menu-item-content {
position: relative;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
height: 36px;
&:hover {
background-color: rgba(231, 0, 11, 0.05);
}
&.active {
background-color: #E7000B;
.menu-item-inner {
color: #FFFFFF;
}
}
}
.menu-item-inner {
display: flex;
align-items: center;
height: 100%;
padding: 0 12px;
color: #0A0A0A;
font-size: 14px;
font-weight: 500;
line-height: 1.43;
}
.tab-icon {
width: 16px;
height: 16px;
margin-right: 12px;
flex-shrink: 0;
object-fit: contain;
}
.menu-title {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.expand-icon {
width: 12px;
height: 12px;
transition: transform 0.3s ease;
margin-left: auto;
flex-shrink: 0;
&.expanded {
transform: rotate(180deg);
}
}
.submenu {
margin: 4px 0;
overflow: hidden;
.menu-item {
margin-bottom: 2px;
}
.menu-item-content {
.menu-item-inner {
padding-left: 40px;
font-size: 14px;
font-weight: 400;
}
}
}
/* 动画效果 */
.submenu-enter-active,
.submenu-leave-active {
transition: all 0.3s ease;
}
.submenu-enter-from,
.submenu-leave-to {
opacity: 0;
transform: translateY(-10px);
}
</style>