244 lines
5.3 KiB
Vue
244 lines
5.3 KiB
Vue
<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>
|