Files
Ticket/web/src/components/AppMenu.vue
T
2026-05-22 15:37:45 +08:00

139 lines
3.2 KiB
Vue

<template>
<n-menu
:collapsed="authStore.layoutCollapsed"
:collapsed-width="84"
:collapsed-icon-size="18"
:options="menuOptions"
:value="activePath"
:indent="16"
@update:value="handleSelect"
/>
</template>
<script setup lang="ts">
import { computed, h, type Component } from 'vue'
import { NIcon, type MenuOption } from 'naive-ui'
import * as LucideIcons from 'lucide-vue-next'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import type { MenuNode } from '@/types/auth'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
function renderIcon(icon?: string | null) {
if (!icon) return undefined
const IconComp = LucideIcons[icon as keyof typeof LucideIcons] as unknown as Component | undefined
if (!IconComp) return undefined
return () => h(NIcon, null, { default: () => h(IconComp) })
}
function toOption(menu: MenuNode): MenuOption | null {
if (!menu.visible || menu.type === 'BUTTON') return null
const children = (menu.children ?? [])
.map((item) => toOption(item))
.filter((item): item is MenuOption => Boolean(item))
const key = menu.path && menu.type !== 'CATALOG' ? menu.path : `catalog-${menu.id}`
return {
key,
label: menu.title,
icon: renderIcon(menu.icon),
children: children.length > 0 ? children : undefined
}
}
const menuOptions = computed(() =>
authStore.menus.map((item) => toOption(item)).filter((item): item is MenuOption => Boolean(item))
)
const activePath = computed(() => route.path)
function handleSelect(key: string) {
if (key.startsWith('catalog-')) return
router.push(key)
}
</script>
<style scoped>
:deep(.n-menu) {
height: 100%;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
font-size: 13px;
background: transparent;
scrollbar-width: none;
-ms-overflow-style: none;
}
:deep(.n-menu::-webkit-scrollbar) {
display: none;
}
:deep(.n-menu-content),
:deep(.n-menu-content-wrapper) {
scrollbar-width: none;
-ms-overflow-style: none;
}
:deep(.n-menu-content::-webkit-scrollbar),
:deep(.n-menu-content-wrapper::-webkit-scrollbar) {
display: none;
}
:deep(.n-menu-item),
:deep(.n-submenu) {
margin: 2px 0;
}
:deep(.n-menu-item-content),
:deep(.n-submenu .n-menu-item-content-header) {
transition:
background 0.16s ease,
color 0.16s ease;
}
:deep(.n-menu-item-content-header),
:deep(.n-menu-item-content__arrow) {
min-width: 0;
}
:deep(.n-menu-item-content-header) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.n-menu-item-content--selected) {
font-weight: 600;
box-shadow: none;
}
:deep(.n-menu-item-content--selected::before) {
background: transparent;
}
:deep(.n-menu--collapsed .n-menu-item-content),
:deep(.n-menu--collapsed .n-submenu .n-menu-item-content-header) {
justify-content: center;
padding-left: 0 !important;
padding-right: 0 !important;
}
:deep(.n-menu--collapsed .n-menu-item-content__icon),
:deep(.n-menu--collapsed .n-submenu .n-menu-item-content-header__icon) {
margin-right: 0 !important;
width: 100%;
display: flex;
justify-content: center;
}
:deep(.n-menu-item-content__icon .n-icon),
:deep(.n-menu-item-content-header__icon .n-icon) {
font-size: 18px;
}
</style>