Files
schoolNews/schoolNewsWeb/src/components/MenuItem.vue
2025-10-07 13:31:06 +08:00

229 lines
4.4 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,
'collapsed': collapsed
}"
@click="toggleExpanded"
>
<div class="menu-item-inner">
<i class="menu-icon" :class="menu.icon || 'icon-folder'"></i>
<span class="menu-title" v-if="!collapsed">{{ menu.name }}</span>
<i
class="expand-icon"
:class="{ 'expanded': expanded }"
v-if="!collapsed"
></i>
</div>
</div>
<!-- 子菜单 -->
<transition name="submenu">
<div
class="submenu"
v-if="expanded && !collapsed"
>
<MenuItem
v-for="child in menu.children"
:key="child.menuID"
:menu="child"
:collapsed="false"
:level="level + 1"
@menu-click="$emit('menu-click', $event)"
/>
</div>
</transition>
</template>
<!-- 没有子菜单的情况 -->
<template v-else>
<div
class="menu-item-content"
:class="{
'active': isActive,
'collapsed': collapsed
}"
@click="handleClick"
>
<div class="menu-item-inner">
<i class="menu-icon" :class="menu.icon || 'icon-file'"></i>
<span class="menu-title" v-if="!collapsed">{{ 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';
// 递归组件需要声明名称Vue 3.5+
defineOptions({
name: 'MenuItem'
});
// Props
interface Props {
menu: SysMenu;
collapsed?: boolean;
level?: number;
}
const props = withDefaults(defineProps<Props>(), {
level: 0
});
// Emits
const emit = defineEmits<{
'menu-click': [menu: SysMenu];
}>();
// 状态
const expanded = ref(false);
// Composition API
const route = useRoute();
// 计算属性
const hasChildren = computed(() => {
return props.menu.children && props.menu.children.length > 0;
});
const isActive = computed(() => {
// 检查当前路由是否匹配此菜单
return route.path === props.menu.url;
});
// 方法 - 使用 function 声明
function toggleExpanded() {
if (hasChildren.value) {
expanded.value = !expanded.value;
}
}
function handleClick() {
if (props.menu.type === MenuType.MENU && props.menu.url) {
emit('menu-click', props.menu);
}
}
</script>
<style lang="scss" scoped>
.menu-item {
margin: 2px 8px;
}
.menu-item-content {
position: relative;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
&.active {
background-color: #1890ff;
.menu-item-inner {
color: white;
}
}
&.collapsed {
margin: 4px 0;
text-align: center;
}
}
.menu-item-inner {
display: flex;
align-items: center;
padding: 12px 16px;
color: rgba(255, 255, 255, 0.85);
font-size: 14px;
line-height: 1;
.collapsed & {
justify-content: center;
padding: 12px 8px;
}
}
.menu-icon {
font-size: 16px;
width: 16px;
text-align: center;
margin-right: 12px;
.collapsed & {
margin-right: 0;
}
}
.menu-title {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.expand-icon {
font-size: 12px;
transition: transform 0.2s ease;
margin-left: auto;
&::before {
content: "▶";
}
&.expanded {
transform: rotate(90deg);
}
}
.submenu {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
margin: 4px 0;
overflow: hidden;
.menu-item-content {
margin: 0 4px;
.menu-item-inner {
padding-left: 48px;
font-size: 13px;
}
}
}
/* 动画效果 */
.submenu-enter-active,
.submenu-leave-active {
transition: all 0.3s ease;
}
.submenu-enter-from,
.submenu-leave-to {
opacity: 0;
transform: translateY(-10px);
}
/* 图标字体类(简单实现,实际项目中使用图标库) */
.icon-folder::before { content: "📁"; }
.icon-file::before { content: "📄"; }
.icon-dashboard::before { content: "📊"; }
.icon-user::before { content: "👤"; }
.icon-news::before { content: "📰"; }
.icon-settings::before { content: "⚙️"; }
</style>