feat: enhance layout and sidebar with new menu items and animations
This commit is contained in:
parent
06c212821a
commit
f151070db0
2
components.d.ts
vendored
2
components.d.ts
vendored
@ -13,11 +13,13 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AppConfig: typeof import('./src/components/layout/AppConfig.vue')['default']
|
||||
AppTopbar: typeof import('./src/components/Holos/AppTopbar.vue')['default']
|
||||
Badge: typeof import('primevue/badge')['default']
|
||||
Button: typeof import('primevue/button')['default']
|
||||
Column: typeof import('primevue/column')['default']
|
||||
DataTable: typeof import('primevue/datatable')['default']
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||
KpiCard: typeof import('./src/components/shared/KpiCard.vue')['default']
|
||||
Menu: typeof import('primevue/menu')['default']
|
||||
Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default']
|
||||
Tag: typeof import('primevue/tag')['default']
|
||||
TopBar: typeof import('./src/components/layout/TopBar.vue')['default']
|
||||
|
||||
@ -15,7 +15,7 @@ import WarehouseDashboard from './modules/warehouse/components/WarehouseDashboar
|
||||
<TopBar />
|
||||
|
||||
<!-- Page Content -->
|
||||
<main class="flex-1 overflow-auto">
|
||||
<main class="flex-1 overflow-auto p-4 lg:p-6">
|
||||
<WarehouseDashboard />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -11,30 +11,106 @@ interface MenuItem {
|
||||
const menuItems = ref<MenuItem[]>([
|
||||
{
|
||||
label: 'Dashboard',
|
||||
icon: 'pi pi-home',
|
||||
icon: 'pi pi-chart-line',
|
||||
to: '/'
|
||||
},
|
||||
{
|
||||
label: 'Almacén',
|
||||
icon: 'pi pi-warehouse',
|
||||
label: 'Ventas',
|
||||
icon: 'pi pi-shopping-cart',
|
||||
items: [
|
||||
{ label: 'Inventario', icon: 'pi pi-box', to: '/warehouse/inventory' },
|
||||
{ label: 'Movimientos', icon: 'pi pi-arrow-right-arrow-left', to: '/warehouse/movements' }
|
||||
{ label: 'Nueva Venta', icon: 'pi pi-plus', to: '/ventas/nueva' },
|
||||
{ label: 'Lista de Ventas', icon: 'pi pi-list', to: '/ventas/lista' },
|
||||
{ label: 'Cotizaciones', icon: 'pi pi-file-edit', to: '/ventas/cotizaciones' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Clientes',
|
||||
icon: 'pi pi-users',
|
||||
to: '/clientes'
|
||||
},
|
||||
{
|
||||
label: 'Inventario',
|
||||
icon: 'pi pi-box',
|
||||
to: '/inventario'
|
||||
},
|
||||
{
|
||||
label: 'Finanzas',
|
||||
icon: 'pi pi-wallet',
|
||||
items: [
|
||||
{ label: 'Ingresos', icon: 'pi pi-arrow-up', to: '/finanzas/ingresos' },
|
||||
{ label: 'Gastos', icon: 'pi pi-credit-card', to: '/finanzas/gastos' },
|
||||
{ label: 'Cuentas por Cobrar', icon: 'pi pi-money-bill', to: '/finanzas/cobrar' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Reportes',
|
||||
icon: 'pi pi-chart-bar',
|
||||
to: '/reportes'
|
||||
},
|
||||
{
|
||||
label: 'Documentos',
|
||||
icon: 'pi pi-file',
|
||||
to: '/documentos'
|
||||
},
|
||||
{
|
||||
label: 'Módulo Personalizado',
|
||||
icon: 'pi pi-th-large',
|
||||
to: '/modulo-personalizado'
|
||||
},
|
||||
{
|
||||
label: 'Configuración',
|
||||
icon: 'pi pi-cog',
|
||||
to: '/settings'
|
||||
to: '/configuracion'
|
||||
}
|
||||
]);
|
||||
|
||||
const sidebarVisible = ref(true);
|
||||
const openItems = ref<string[]>([]);
|
||||
const currentRoute = ref(window.location.pathname);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
sidebarVisible.value = !sidebarVisible.value;
|
||||
};
|
||||
|
||||
const toggleItem = (label: string) => {
|
||||
const index = openItems.value.indexOf(label);
|
||||
if (index > -1) {
|
||||
openItems.value.splice(index, 1);
|
||||
} else {
|
||||
openItems.value.push(label);
|
||||
}
|
||||
};
|
||||
|
||||
const isItemOpen = (label: string) => {
|
||||
return openItems.value.includes(label);
|
||||
};
|
||||
|
||||
const isRouteActive = (to: string | undefined) => {
|
||||
if (!to) return false;
|
||||
return currentRoute.value === to;
|
||||
};
|
||||
|
||||
// Simular cambio de ruta (en producción usarías Vue Router)
|
||||
const navigateTo = (to: string) => {
|
||||
currentRoute.value = to;
|
||||
// window.history.pushState({}, '', to);
|
||||
};
|
||||
|
||||
// Funciones de animación para los submenús
|
||||
const onEnter = (el: Element) => {
|
||||
const element = el as HTMLElement;
|
||||
element.style.height = '0';
|
||||
element.offsetHeight; // Force reflow
|
||||
element.style.height = element.scrollHeight + 'px';
|
||||
};
|
||||
|
||||
const onLeave = (el: Element) => {
|
||||
const element = el as HTMLElement;
|
||||
element.style.height = element.scrollHeight + 'px';
|
||||
element.offsetHeight; // Force reflow
|
||||
element.style.height = '0';
|
||||
};
|
||||
|
||||
defineExpose({ toggleSidebar });
|
||||
</script>
|
||||
|
||||
@ -47,15 +123,22 @@ defineExpose({ toggleSidebar });
|
||||
>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Logo / Brand -->
|
||||
<div class="p-4 border-b border-surface-200 dark:border-surface-700">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="pi pi-box text-2xl text-primary"></i>
|
||||
<span
|
||||
v-if="sidebarVisible"
|
||||
class="text-lg font-semibold text-surface-900 dark:text-surface-0"
|
||||
<div
|
||||
class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary transition-all"
|
||||
:class="sidebarVisible ? 'bg-primary' : 'bg-primary/90'"
|
||||
>
|
||||
GOLS Control
|
||||
</span>
|
||||
<i class="pi pi-chart-line text-xl text-white"></i>
|
||||
</div>
|
||||
<div v-if="sidebarVisible" class="flex flex-col">
|
||||
<h1 class="text-xl font-bold text-surface-900 dark:text-surface-0">
|
||||
Golscontrols
|
||||
</h1>
|
||||
<p class="text-xs text-surface-500 dark:text-surface-400">
|
||||
Sistema ERP
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -67,29 +150,69 @@ defineExpose({ toggleSidebar });
|
||||
<a
|
||||
v-if="!item.items"
|
||||
:href="item.to"
|
||||
class="flex items-center gap-3 px-3 py-2 rounded-lg text-surface-700 dark:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
|
||||
@click.prevent="item.to && navigateTo(item.to)"
|
||||
:class="[
|
||||
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all cursor-pointer',
|
||||
isRouteActive(item.to)
|
||||
? 'bg-primary text-white shadow-sm'
|
||||
: 'text-surface-700 dark:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800'
|
||||
]"
|
||||
:title="!sidebarVisible ? item.label : ''"
|
||||
>
|
||||
<i :class="item.icon"></i>
|
||||
<i :class="[item.icon, 'shrink-0']"></i>
|
||||
<span v-if="sidebarVisible">{{ item.label }}</span>
|
||||
</a>
|
||||
|
||||
<!-- Item con subitems -->
|
||||
<!-- Item con subitems (Collapsible) -->
|
||||
<div v-else>
|
||||
<div class="flex items-center gap-3 px-3 py-2 text-surface-700 dark:text-surface-200 font-medium">
|
||||
<i :class="item.icon"></i>
|
||||
<span v-if="sidebarVisible">{{ item.label }}</span>
|
||||
</div>
|
||||
<ul v-if="sidebarVisible" class="ml-6 mt-1 space-y-1">
|
||||
<li v-for="subItem in item.items" :key="subItem.label">
|
||||
<a
|
||||
:href="subItem.to"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg text-surface-600 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors text-sm"
|
||||
>
|
||||
<i :class="subItem.icon" class="text-xs"></i>
|
||||
<span>{{ subItem.label }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
@click="toggleItem(item.label)"
|
||||
:class="[
|
||||
'w-full flex items-center justify-between px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
'text-surface-700 dark:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800'
|
||||
]"
|
||||
:title="!sidebarVisible ? item.label : ''"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<i :class="[item.icon, 'shrink-0']"></i>
|
||||
<span v-if="sidebarVisible">{{ item.label }}</span>
|
||||
</div>
|
||||
<i
|
||||
v-if="sidebarVisible"
|
||||
:class="[
|
||||
'pi pi-chevron-down text-xs transition-transform duration-200',
|
||||
isItemOpen(item.label) && 'rotate-180'
|
||||
]"
|
||||
></i>
|
||||
</button>
|
||||
|
||||
<!-- Subitems con animación -->
|
||||
<Transition
|
||||
name="submenu"
|
||||
@enter="onEnter"
|
||||
@leave="onLeave"
|
||||
>
|
||||
<ul
|
||||
v-if="sidebarVisible && isItemOpen(item.label)"
|
||||
class="ml-6 mt-1 space-y-1 overflow-hidden"
|
||||
>
|
||||
<li v-for="subItem in item.items" :key="subItem.label">
|
||||
<a
|
||||
:href="subItem.to"
|
||||
@click.prevent="subItem.to && navigateTo(subItem.to)"
|
||||
:class="[
|
||||
'flex items-center gap-2 px-3 py-2 pl-6 rounded-lg text-sm font-medium transition-all cursor-pointer',
|
||||
isRouteActive(subItem.to)
|
||||
? 'bg-primary text-white shadow-sm'
|
||||
: 'text-surface-600 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-800'
|
||||
]"
|
||||
>
|
||||
<i :class="[subItem.icon, 'text-xs shrink-0']"></i>
|
||||
<span>{{ subItem.label }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Transition>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@ -100,9 +223,10 @@ defineExpose({ toggleSidebar });
|
||||
<button
|
||||
@click="toggleSidebar"
|
||||
class="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-surface-700 dark:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
|
||||
:title="sidebarVisible ? 'Contraer sidebar' : 'Expandir sidebar'"
|
||||
>
|
||||
<i :class="sidebarVisible ? 'pi pi-angle-left' : 'pi pi-angle-right'"></i>
|
||||
<span v-if="sidebarVisible">Contraer</span>
|
||||
<span v-if="sidebarVisible" class="text-sm font-medium">Contraer</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -114,5 +238,57 @@ aside {
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
/* Animación para submenús */
|
||||
.submenu-enter-active,
|
||||
.submenu-leave-active {
|
||||
transition: height 0.3s ease, opacity 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.submenu-enter-from,
|
||||
.submenu-leave-to {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.submenu-enter-to,
|
||||
.submenu-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Animación de rotación para el chevron */
|
||||
.rotate-180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Efecto de sombra en item activo */
|
||||
.shadow-sm {
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Scroll suave */
|
||||
nav {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(155, 155, 155, 0.5) transparent;
|
||||
}
|
||||
|
||||
nav::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
nav::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
nav::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(155, 155, 155, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
nav::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(155, 155, 155, 0.7);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,32 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useLayout } from "../../composables/useLayout";
|
||||
import AppConfig from "./AppConfig.vue";
|
||||
|
||||
const { isDarkMode, toggleDarkMode } = useLayout();
|
||||
|
||||
// Referencia al menú de usuario
|
||||
const userMenu = ref();
|
||||
|
||||
// Función para toggle del menú
|
||||
const toggleUserMenu = (event: Event) => {
|
||||
userMenu.value.toggle(event);
|
||||
};
|
||||
|
||||
// Opciones del menú de usuario
|
||||
const userMenuItems = ref([
|
||||
{
|
||||
label: 'Mi Perfil',
|
||||
icon: 'pi pi-user',
|
||||
command: () => {
|
||||
console.log('Ir a perfil');
|
||||
// Aquí puedes agregar la navegación: router.push('/profile')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Configuración',
|
||||
icon: 'pi pi-cog',
|
||||
command: () => {
|
||||
console.log('Ir a configuración');
|
||||
// Aquí puedes agregar la navegación: router.push('/settings')
|
||||
}
|
||||
},
|
||||
{
|
||||
separator: true
|
||||
},
|
||||
{
|
||||
label: 'Cerrar Sesión',
|
||||
icon: 'pi pi-sign-out',
|
||||
command: () => {
|
||||
console.log('Cerrar sesión');
|
||||
// Aquí puedes agregar la lógica de logout
|
||||
// Por ejemplo: authStore.logout() o router.push('/login')
|
||||
}
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-surface-0 dark:bg-surface-900 border-b border-surface-200 dark:border-surface-700 px-6 py-4">
|
||||
<header class="bg-surface-0 dark:bg-surface-900 border-b border-surface-200 dark:border-surface-700 px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Left Section: Branding -->
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xl font-semibold text-surface-900 dark:text-surface-0">
|
||||
GOLS Control
|
||||
</span>
|
||||
<!-- <i class="pi pi-box text-2xl text-primary"></i>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-lg font-bold text-surface-900 dark:text-surface-0">
|
||||
GOLS Control
|
||||
</span>
|
||||
<span class="text-xs text-surface-500 dark:text-surface-400">
|
||||
Sistema de Gestión
|
||||
</span>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Right Section: Actions -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Botón de modo oscuro -->
|
||||
<!-- Search Button (opcional) -->
|
||||
<!-- <button
|
||||
type="button"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-surface-100 dark:hover:bg-surface-800 transition-all text-surface-700 dark:text-surface-200"
|
||||
title="Buscar"
|
||||
>
|
||||
<i class="pi pi-search"></i>
|
||||
</button> -->
|
||||
|
||||
<!-- Notifications Button (opcional) -->
|
||||
<!-- <button
|
||||
type="button"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-surface-100 dark:hover:bg-surface-800 transition-all text-surface-700 dark:text-surface-200 relative"
|
||||
title="Notificaciones"
|
||||
>
|
||||
<i class="pi pi-bell"></i>
|
||||
<span class="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
|
||||
</button> -->
|
||||
|
||||
<!-- Dark Mode Toggle -->
|
||||
<button
|
||||
type="button"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-surface-100 dark:hover:bg-surface-800 transition-all text-surface-900 dark:text-surface-0 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 dark:focus-visible:ring-offset-surface-950"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-surface-100 dark:hover:bg-surface-800 transition-all text-surface-900 dark:text-surface-0"
|
||||
@click="toggleDarkMode"
|
||||
:title="isDarkMode ? 'Modo claro' : 'Modo oscuro'"
|
||||
>
|
||||
<i :class="['pi', isDarkMode ? 'pi-sun' : 'pi-moon']" />
|
||||
</button>
|
||||
|
||||
<!-- Botón de configuración de colores -->
|
||||
<div class="relative">
|
||||
<!-- Settings/Config Button -->
|
||||
<!-- <div class="relative">
|
||||
<button
|
||||
v-styleclass="{
|
||||
selector: '@next',
|
||||
@ -37,16 +105,67 @@ const { isDarkMode, toggleDarkMode } = useLayout();
|
||||
hideOnOutsideClick: true,
|
||||
}"
|
||||
type="button"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-surface-100 dark:hover:bg-surface-800 transition-all text-surface-900 dark:text-surface-0 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 dark:focus-visible:ring-offset-surface-950"
|
||||
title="Configurar colores"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-surface-100 dark:hover:bg-surface-800 transition-all text-surface-700 dark:text-surface-200"
|
||||
title="Configuración"
|
||||
>
|
||||
<i class="pi pi-palette" />
|
||||
<i class="pi pi-cog" />
|
||||
</button>
|
||||
<AppConfig />
|
||||
</div> -->
|
||||
|
||||
<!-- User Profile Button with Menu -->
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-surface-100 dark:hover:bg-surface-800 transition-all"
|
||||
title="Perfil"
|
||||
@click="toggleUserMenu"
|
||||
aria-haspopup="true"
|
||||
aria-controls="user_menu"
|
||||
>
|
||||
<div class="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white text-sm font-semibold">
|
||||
JD
|
||||
</div>
|
||||
<span class="text-sm font-medium text-surface-900 dark:text-surface-0 hidden md:block">
|
||||
John Doe
|
||||
</span>
|
||||
<i class="pi pi-angle-down text-sm text-surface-500 hidden md:block"></i>
|
||||
</button>
|
||||
|
||||
<!-- Menu de Usuario -->
|
||||
<Menu
|
||||
ref="userMenu"
|
||||
id="user_menu"
|
||||
:model="userMenuItems"
|
||||
:popup="true"
|
||||
class="w-56"
|
||||
>
|
||||
<template #start>
|
||||
<div class="px-4 py-3 border-b border-surface-200 dark:border-surface-700">
|
||||
<p class="text-sm font-semibold text-surface-900 dark:text-surface-0">John Doe</p>
|
||||
<p class="text-xs text-surface-500 dark:text-surface-400">john.doe@example.com</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #item="{ item, props }">
|
||||
<a
|
||||
v-bind="props.action"
|
||||
class="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
|
||||
:class="{ 'text-red-600 dark:text-red-400': item.label === 'Cerrar Sesión' }"
|
||||
>
|
||||
<i :class="item.icon"></i>
|
||||
<span class="text-sm font-medium">{{ item.label }}</span>
|
||||
</a>
|
||||
</template>
|
||||
<template #end>
|
||||
<div class="px-4 py-2 border-t border-surface-200 dark:border-surface-700">
|
||||
<p class="text-xs text-surface-400 dark:text-surface-500">Versión 1.0.0</p>
|
||||
</div>
|
||||
</template>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@ -63,6 +182,7 @@ const { isDarkMode, toggleDarkMode } = useLayout();
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
@ -73,6 +193,7 @@ const { isDarkMode, toggleDarkMode } = useLayout();
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user