feat: layouts auth/default e componentes AppSidebar/AppTopbar
This commit is contained in:
@@ -1,78 +1,5 @@
|
|||||||
<script setup>
|
|
||||||
useHead({
|
|
||||||
meta: [
|
|
||||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
|
|
||||||
],
|
|
||||||
link: [
|
|
||||||
{ rel: 'icon', href: '/favicon.ico' }
|
|
||||||
],
|
|
||||||
htmlAttrs: {
|
|
||||||
lang: 'en'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const title = 'Nuxt Starter Template'
|
|
||||||
const description = 'A production-ready starter template powered by Nuxt UI. Build beautiful, accessible, and performant applications in minutes, not hours.'
|
|
||||||
|
|
||||||
useSeoMeta({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
ogTitle: title,
|
|
||||||
ogDescription: description,
|
|
||||||
ogImage: 'https://ui.nuxt.com/assets/templates/nuxt/starter-light.png',
|
|
||||||
twitterImage: 'https://ui.nuxt.com/assets/templates/nuxt/starter-light.png',
|
|
||||||
twitterCard: 'summary_large_image'
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UApp>
|
<NuxtLayout>
|
||||||
<UHeader>
|
<NuxtPage />
|
||||||
<template #left>
|
</NuxtLayout>
|
||||||
<NuxtLink to="/">
|
|
||||||
<AppLogo class="w-auto h-6 shrink-0" />
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<TemplateMenu />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #right>
|
|
||||||
<UColorModeButton />
|
|
||||||
|
|
||||||
<UButton
|
|
||||||
to="https://github.com/nuxt-ui-templates/starter"
|
|
||||||
target="_blank"
|
|
||||||
icon="i-simple-icons-github"
|
|
||||||
aria-label="GitHub"
|
|
||||||
color="neutral"
|
|
||||||
variant="ghost"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</UHeader>
|
|
||||||
|
|
||||||
<UMain>
|
|
||||||
<NuxtPage />
|
|
||||||
</UMain>
|
|
||||||
|
|
||||||
<USeparator icon="i-simple-icons-nuxtdotjs" />
|
|
||||||
|
|
||||||
<UFooter>
|
|
||||||
<template #left>
|
|
||||||
<p class="text-sm text-muted">
|
|
||||||
Built with Nuxt UI • © {{ new Date().getFullYear() }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #right>
|
|
||||||
<UButton
|
|
||||||
to="https://github.com/nuxt-ui-templates/starter"
|
|
||||||
target="_blank"
|
|
||||||
icon="i-simple-icons-github"
|
|
||||||
aria-label="GitHub"
|
|
||||||
color="neutral"
|
|
||||||
variant="ghost"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</UFooter>
|
|
||||||
</UApp>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
177
front-end/app/components/AppSidebar.vue
Normal file
177
front-end/app/components/AppSidebar.vue
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { editais } from '~/data/mock/editais'
|
||||||
|
import { prazos } from '~/data/mock/prazos'
|
||||||
|
import { documentos } from '~/data/mock/documentos'
|
||||||
|
|
||||||
|
const { user, logout } = useAuth()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const contagemPorStatus = computed(() => {
|
||||||
|
const counts: Record<string, number> = {}
|
||||||
|
for (const e of editais) {
|
||||||
|
counts[e.status] = (counts[e.status] ?? 0) + 1
|
||||||
|
}
|
||||||
|
return counts
|
||||||
|
})
|
||||||
|
|
||||||
|
const alertasPrazos = computed(() => prazos.filter(p => p.urgencia === 'critico' || p.urgencia === 'urgente').length)
|
||||||
|
const docsVencendo = computed(() => documentos.filter(d => d.status === 'vencendo' || d.status === 'vencida').length)
|
||||||
|
|
||||||
|
const navItems = computed(() => [
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
items: [
|
||||||
|
{ label: 'Dashboard', icon: 'i-heroicons-home', to: '/' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Oportunidades',
|
||||||
|
items: [
|
||||||
|
{ label: 'Todos os Editais', icon: 'i-heroicons-clipboard-document-list', to: '/oportunidades', badge: editais.length, badgeVariant: 'default' },
|
||||||
|
{ label: 'Em Análise', icon: 'i-heroicons-magnifying-glass', to: '/oportunidades/em-analise', badge: contagemPorStatus.value.em_analise ?? 0, badgeVariant: 'default' },
|
||||||
|
{ label: 'Elaborando Proposta', icon: 'i-heroicons-pencil-square', to: '/oportunidades/elaborando-proposta', badge: contagemPorStatus.value.elaborando_proposta ?? 0, badgeVariant: 'warning' },
|
||||||
|
{ label: 'Participando', icon: 'i-heroicons-play', to: '/oportunidades/participando', badge: contagemPorStatus.value.participando ?? 0, badgeVariant: 'default' },
|
||||||
|
{ label: 'Recurso', icon: 'i-heroicons-scale', to: '/oportunidades/recurso', badge: contagemPorStatus.value.recurso ?? 0, badgeVariant: 'warning' },
|
||||||
|
{ label: 'Vencidas', icon: 'i-heroicons-trophy', to: '/oportunidades/vencidas', badge: contagemPorStatus.value.vencida ?? 0, badgeVariant: 'success' },
|
||||||
|
{ label: 'Perdidas', icon: 'i-heroicons-x-circle', to: '/oportunidades/perdidas', badge: contagemPorStatus.value.perdida ?? 0, badgeVariant: 'neutral' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pipeline',
|
||||||
|
items: [
|
||||||
|
{ label: 'Kanban de Processos', icon: 'i-heroicons-view-columns', to: '/pipeline' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Gestão',
|
||||||
|
items: [
|
||||||
|
{ label: 'Documentos', icon: 'i-heroicons-folder', to: '/gestao/documentos', badge: docsVencendo.value || undefined, badgeVariant: 'warning' },
|
||||||
|
{ label: 'Prazos', icon: 'i-heroicons-clock', to: '/gestao/prazos', badge: alertasPrazos.value || undefined, badgeVariant: 'warning' },
|
||||||
|
{ label: 'Órgãos Públicos', icon: 'i-heroicons-building-library', to: '/gestao/orgaos' },
|
||||||
|
{ label: 'Concorrentes', icon: 'i-heroicons-building-office-2', to: '/gestao/concorrentes' },
|
||||||
|
{ label: 'Contratos', icon: 'i-heroicons-document-text', to: '/gestao/contratos' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Inteligência',
|
||||||
|
items: [
|
||||||
|
{ label: 'Inteligência de Mercado', icon: 'i-heroicons-chart-bar', to: '/inteligencia' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sistema',
|
||||||
|
items: [
|
||||||
|
{ label: 'Usuários', icon: 'i-heroicons-users', to: '/sistema/usuarios' },
|
||||||
|
{ label: 'Configurações', icon: 'i-heroicons-cog-6-tooth', to: '/sistema/configuracoes' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
function isActive(to: string) {
|
||||||
|
return route.path === to
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<div class="logo-icon">
|
||||||
|
<UIcon name="i-heroicons-home" class="text-white w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<span class="logo-name">Licitatche</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<template v-for="group in navItems" :key="group.label">
|
||||||
|
<p v-if="group.label" class="nav-label">{{ group.label }}</p>
|
||||||
|
<NuxtLink
|
||||||
|
v-for="item in group.items"
|
||||||
|
:key="item.to"
|
||||||
|
:to="item.to"
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: isActive(item.to) }"
|
||||||
|
>
|
||||||
|
<UIcon :name="item.icon" class="nav-icon" />
|
||||||
|
<span class="nav-text">{{ item.label }}</span>
|
||||||
|
<UBadge
|
||||||
|
v-if="item.badge"
|
||||||
|
:label="String(item.badge)"
|
||||||
|
variant="soft"
|
||||||
|
:color="item.badgeVariant === 'warning' ? 'warning' : item.badgeVariant === 'success' ? 'success' : item.badgeVariant === 'neutral' ? 'neutral' : 'primary'"
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
|
<div v-if="group.label" class="nav-divider" />
|
||||||
|
</template>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="user-row">
|
||||||
|
<UAvatar :alt="user?.nome ?? 'A'" size="xs" />
|
||||||
|
<div class="user-info">
|
||||||
|
<p class="user-name">{{ user?.nome }}</p>
|
||||||
|
<p class="user-role">{{ user?.papel }}</p>
|
||||||
|
</div>
|
||||||
|
<UButton icon="i-heroicons-arrow-right-on-rectangle" variant="ghost" color="neutral" size="xs" @click="logout" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sidebar {
|
||||||
|
width: 232px;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0f172a;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.sidebar-logo {
|
||||||
|
padding: 18px 14px 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
border-bottom: 1px solid #1e293b;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: #0f172a;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.logo-icon {
|
||||||
|
width: 32px; height: 32px;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.logo-name { font-size: 14px; font-weight: 700; color: #f1f5f9; }
|
||||||
|
.sidebar-nav { padding: 8px; flex: 1; }
|
||||||
|
.nav-label {
|
||||||
|
font-size: 9.5px; font-weight: 700; color: #475569;
|
||||||
|
text-transform: uppercase; letter-spacing: 1px;
|
||||||
|
padding: 8px 8px 3px;
|
||||||
|
}
|
||||||
|
.nav-item {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 7px 8px; border-radius: 7px;
|
||||||
|
color: #94a3b8; text-decoration: none;
|
||||||
|
font-size: 12.5px; margin-bottom: 1px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.nav-item:hover { background: #1e293b; color: #e2e8f0; }
|
||||||
|
.nav-item.active { background: #1e3a5f; color: #93c5fd; }
|
||||||
|
.nav-icon { width: 15px; height: 15px; flex-shrink: 0; }
|
||||||
|
.nav-text { flex: 1; }
|
||||||
|
.nav-divider { height: 1px; background: #1e293b; margin: 4px 0; }
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 10px 8px;
|
||||||
|
border-top: 1px solid #1e293b;
|
||||||
|
position: sticky; bottom: 0; background: #0f172a;
|
||||||
|
}
|
||||||
|
.user-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 7px; }
|
||||||
|
.user-row:hover { background: #1e293b; }
|
||||||
|
.user-info { flex: 1; }
|
||||||
|
.user-name { font-size: 12px; font-weight: 600; color: #e2e8f0; }
|
||||||
|
.user-role { font-size: 10.5px; color: #64748b; }
|
||||||
|
</style>
|
||||||
33
front-end/app/components/AppTopbar.vue
Normal file
33
front-end/app/components/AppTopbar.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
breadcrumb?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="topbar">
|
||||||
|
<div>
|
||||||
|
<h1 class="topbar-title">{{ title }}</h1>
|
||||||
|
<p v-if="breadcrumb" class="topbar-breadcrumb">{{ breadcrumb }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-actions">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.topbar {
|
||||||
|
background: white;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
padding: 13px 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.topbar-title { font-size: 16px; font-weight: 700; color: #0f172a; }
|
||||||
|
.topbar-breadcrumb { font-size: 11px; color: #94a3b8; margin-top: 1px; }
|
||||||
|
.topbar-actions { display: flex; gap: 8px; align-items: center; }
|
||||||
|
</style>
|
||||||
28
front-end/app/layouts/auth.vue
Normal file
28
front-end/app/layouts/auth.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<div class="auth-layout">
|
||||||
|
<div class="bg-circle bg-circle-1" />
|
||||||
|
<div class="bg-circle bg-circle-2" />
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auth-layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.bg-circle {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.bg-circle-1 { width: 400px; height: 400px; top: -100px; right: -100px; }
|
||||||
|
.bg-circle-2 { width: 300px; height: 300px; bottom: -80px; left: -80px; }
|
||||||
|
</style>
|
||||||
23
front-end/app/layouts/default.vue
Normal file
23
front-end/app/layouts/default.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-layout">
|
||||||
|
<AppSidebar />
|
||||||
|
<div class="app-main">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-layout {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
.app-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user