x-control-panel/web-ui/vue-app/src/components/NavigationMenu.vue

244 lines
5.3 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>
<!-- Мобильное меню - как Android шторка с плитками -->
<v-navigation-drawer
v-if="isMobile"
v-model="menuOpen"
temporary
location="top"
class="mobile-menu-drawer"
>
<v-container fluid class="pa-4">
<v-row dense>
<v-col v-for="item in menuItems" :key="item.path" cols="4" class="d-flex justify-center">
<v-card
class="menu-tile text-center"
:color="currentPath === item.path ? 'primary' : 'surface-variant'"
elevation="2"
@click="navigateTo(item.path)"
width="100%"
>
<v-card-text class="pa-3">
<v-icon
size="32"
:icon="item.icon"
:color="currentPath === item.path ? 'white' : '#757575'"
/>
<div
class="text-caption mt-2 font-weight-medium"
:class="currentPath === item.path ? 'text-white' : ''"
:style="currentPath === item.path ? {} : { color: '#757575' }"
>
{{ item.title }}
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</v-navigation-drawer>
<!-- Десктопное меню - обычный div -->
<div
v-if="!isMobile"
class="desktop-menu"
:class="{ collapsed: collapsed }"
:style="{ width: collapsed ? '80px' : '250px' }"
>
<div class="menu-header">
<v-btn
v-if="!collapsed"
icon="mdi-chevron-left"
variant="text"
@click="toggleCollapsed"
class="collapse-btn"
size="small"
/>
<v-btn
v-else
icon="mdi-chevron-right"
variant="text"
@click="toggleCollapsed"
class="collapse-btn"
size="small"
/>
</div>
<v-list v-model="selectedItem" class="menu-list">
<v-list-item
v-for="item in menuItems"
:key="item.path"
:prepend-icon="collapsed ? item.icon : undefined"
:title="collapsed ? '' : item.title"
:value="item.path"
@click="navigateTo(item.path)"
class="menu-item"
>
<template v-if="!collapsed" #prepend>
<v-icon :icon="item.icon" />
</template>
</v-list-item>
</v-list>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAppStore } from '@/stores/app'
interface MenuItem {
title: string
icon: string
path: string
}
const menuItems = computed(() => [
{
title: getUserDisplayName(),
icon: 'mdi-account',
path: '/profile'
},
{ title: 'Настройки', icon: 'mdi-cog', path: '/settings' },
{ title: 'Панель управления', icon: 'mdi-view-dashboard', path: '/dashboard' },
{ title: 'Tree Demo', icon: 'mdi-tree', path: '/tree-demo' }
])
const router = useRouter()
const route = useRoute()
const appStore = useAppStore()
const menuOpen = ref(false)
const collapsed = ref(false)
const isMobile = ref(window.innerWidth < 768)
const currentPath = computed(() => route.path)
const selectedItem = computed(() => currentPath.value)
const getUserDisplayName = () => {
const user = appStore.currentUser
if (user?.name && user.name.trim()) {
return user.name
}
return user?.login || 'Профиль'
}
const emit = defineEmits<{
updateCollapsed: [value: boolean]
toggleMenu: []
}>()
const navigateTo = (path: string) => {
router.push(path)
if (isMobile.value) {
menuOpen.value = false
}
}
const toggleCollapsed = () => {
collapsed.value = !collapsed.value
emit('updateCollapsed', collapsed.value)
}
const checkMobile = () => {
isMobile.value = window.innerWidth < 768
if (!isMobile.value) {
menuOpen.value = false
}
}
onMounted(() => {
window.addEventListener('resize', checkMobile)
})
onUnmounted(() => {
window.removeEventListener('resize', checkMobile)
})
const toggleMobileMenu = () => {
emit('toggleMenu')
}
defineExpose({
toggle: () => {
menuOpen.value = !menuOpen.value
}
})
</script>
<style scoped>
.mobile-menu-drawer {
z-index: 1001;
height: auto !important;
max-height: 50vh;
border-radius: 0 0 16px 16px;
}
.desktop-menu {
background: rgb(var(--v-theme-surface));
border-right: 1px solid rgb(var(--v-theme-outline));
height: 100vh;
position: fixed;
left: 0;
top: 0;
z-index: 1000;
transition: width 0.3s ease;
}
.desktop-menu.collapsed {
width: 80px !important;
}
.menu-tile {
cursor: pointer;
transition: all 0.2s ease;
aspect-ratio: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.menu-tile:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
}
.menu-tile:active {
transform: translateY(-2px);
}
.menu-header {
padding: 16px 8px 8px 8px;
display: flex;
justify-content: flex-end;
}
.collapse-btn {
z-index: 10;
}
.menu-list {
padding: 8px 4px;
}
.menu-item {
margin: 2px 4px;
border-radius: 6px;
}
.menu-item :deep(.v-list-item__content) {
padding-left: 4px;
}
.menu-item :deep(.v-list-item-title) {
text-align: left;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.mobile-menu-drawer {
width: 100% !important;
}
}
</style>