Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions
+200 -165
View File
@@ -15,245 +15,280 @@
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ref, onMounted, onBeforeUnmount } from 'vue';
const isOnline = ref(true) // começa como true; detecta em onMounted
const wasOffline = ref(false)
const showReconnected = ref(false)
const isOnline = ref(true); // começa como true; detecta em onMounted
const wasOffline = ref(false);
const showReconnected = ref(false);
let pollTimer = null
let reconnectedTimer = null
let pollTimer = null;
let reconnectedTimer = null;
// ── Detecção real: tenta buscar um recurso minúsculo ──────────
async function checkConnectivity () {
try {
// favicon do próprio app (cache busted) — não depende de rede externa
await fetch('/favicon.ico?_t=' + Date.now(), {
method: 'HEAD',
cache: 'no-store',
signal: AbortSignal.timeout(4000)
})
setOnline()
} catch {
setOffline()
}
async function checkConnectivity() {
try {
// favicon do próprio app (cache busted) — não depende de rede externa
await fetch('/favicon.ico?_t=' + Date.now(), {
method: 'HEAD',
cache: 'no-store',
signal: AbortSignal.timeout(4000)
});
setOnline();
} catch {
setOffline();
}
}
function setOnline () {
if (!isOnline.value && wasOffline.value) {
// acabou de reconectar
showReconnected.value = true
if (reconnectedTimer) clearTimeout(reconnectedTimer)
reconnectedTimer = setTimeout(() => { showReconnected.value = false }, 4000)
}
isOnline.value = true
function setOnline() {
if (!isOnline.value && wasOffline.value) {
// acabou de reconectar
showReconnected.value = true;
if (reconnectedTimer) clearTimeout(reconnectedTimer);
reconnectedTimer = setTimeout(() => {
showReconnected.value = false;
}, 4000);
}
isOnline.value = true;
}
function setOffline () {
if (isOnline.value) wasOffline.value = true
showReconnected.value = false
if (reconnectedTimer) clearTimeout(reconnectedTimer)
isOnline.value = false
function setOffline() {
if (isOnline.value) wasOffline.value = true;
showReconnected.value = false;
if (reconnectedTimer) clearTimeout(reconnectedTimer);
isOnline.value = false;
}
// ── Eventos nativos do browser ────────────────────────────────
function onBrowserOffline () { setOffline() }
function onBrowserOnline () { checkConnectivity() } // confirma antes de marcar online
function onBrowserOffline() {
setOffline();
}
function onBrowserOnline() {
checkConnectivity();
} // confirma antes de marcar online
onMounted(() => {
window.addEventListener('offline', onBrowserOffline)
window.addEventListener('online', onBrowserOnline)
window.addEventListener('offline', onBrowserOffline);
window.addEventListener('online', onBrowserOnline);
// Polling a cada 10 s — captura quedas que não disparam evento
pollTimer = setInterval(checkConnectivity, 10_000)
// Polling a cada 10 s — captura quedas que não disparam evento
pollTimer = setInterval(checkConnectivity, 10_000);
// Verifica estado atual ao montar (útil se já começou offline)
checkConnectivity()
})
// Verifica estado atual ao montar (útil se já começou offline)
checkConnectivity();
});
onBeforeUnmount(() => {
window.removeEventListener('offline', onBrowserOffline)
window.removeEventListener('online', onBrowserOnline)
clearInterval(pollTimer)
clearTimeout(reconnectedTimer)
})
window.removeEventListener('offline', onBrowserOffline);
window.removeEventListener('online', onBrowserOnline);
clearInterval(pollTimer);
clearTimeout(reconnectedTimer);
});
</script>
<template>
<Teleport to="body">
<!-- Overlay: sem internet -->
<Transition name="offline-fade">
<div
v-if="!isOnline"
class="offline-overlay"
role="alertdialog"
aria-live="assertive"
aria-label="Sem conexão com a internet"
>
<div class="offline-backdrop" />
<Teleport to="body">
<!-- Overlay: sem internet -->
<Transition name="offline-fade">
<div v-if="!isOnline" class="offline-overlay" role="alertdialog" aria-live="assertive" aria-label="Sem conexão com a internet">
<div class="offline-backdrop" />
<div class="offline-card">
<div class="offline-icon-wrap">
<span class="offline-icon-ring" />
<i class="pi pi-wifi offline-icon" />
</div>
<div class="offline-card">
<div class="offline-icon-wrap">
<span class="offline-icon-ring" />
<i class="pi pi-wifi offline-icon" />
</div>
<h2 class="offline-title">Sem conexão</h2>
<p class="offline-desc">
Verifique sua internet.<br>Tentando reconectar automaticamente
</p>
<h2 class="offline-title">Sem conexão</h2>
<p class="offline-desc">Verifique sua internet.<br />Tentando reconectar automaticamente</p>
<div class="offline-pulse-bar">
<span class="offline-pulse-dot" />
<span class="offline-pulse-dot" style="animation-delay:.2s" />
<span class="offline-pulse-dot" style="animation-delay:.4s" />
</div>
</div>
</div>
</Transition>
<div class="offline-pulse-bar">
<span class="offline-pulse-dot" />
<span class="offline-pulse-dot" style="animation-delay: 0.2s" />
<span class="offline-pulse-dot" style="animation-delay: 0.4s" />
</div>
</div>
</div>
</Transition>
<!-- Toast: reconectou -->
<Transition name="reconnect-toast">
<div v-if="showReconnected" class="reconnect-toast" role="status">
<i class="pi pi-check-circle" />
<span>Conexão restabelecida</span>
</div>
</Transition>
</Teleport>
<!-- Toast: reconectou -->
<Transition name="reconnect-toast">
<div v-if="showReconnected" class="reconnect-toast" role="status">
<i class="pi pi-check-circle" />
<span>Conexão restabelecida</span>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
/* ── Overlay ─────────────────────────────────────────────── */
.offline-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.offline-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
/* ── Card ────────────────────────────────────────────────── */
.offline-card {
position: relative;
z-index: 1;
background: var(--surface-card, #fff);
border: 1px solid var(--surface-border, #e0e0e0);
border-radius: 20px;
padding: 40px 48px;
max-width: 380px;
width: 100%;
text-align: center;
box-shadow: 0 24px 60px rgba(0, 0, 0, .25);
position: relative;
z-index: 1;
background: var(--surface-card, #fff);
border: 1px solid var(--surface-border, #e0e0e0);
border-radius: 20px;
padding: 40px 48px;
max-width: 380px;
width: 100%;
text-align: center;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.25);
}
/* ── Ícone ───────────────────────────────────────────────── */
.offline-icon-wrap {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
margin-bottom: 24px;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
margin-bottom: 24px;
}
.offline-icon-ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid var(--primary-color, #6366f1);
opacity: 0.25;
animation: ring-pulse 2s ease-in-out infinite;
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid var(--primary-color, #6366f1);
opacity: 0.25;
animation: ring-pulse 2s ease-in-out infinite;
}
.offline-icon {
font-size: 2.2rem;
color: var(--primary-color, #6366f1);
opacity: 0.85;
position: relative;
font-size: 2.2rem;
color: var(--primary-color, #6366f1);
opacity: 0.85;
position: relative;
}
@keyframes ring-pulse {
0%, 100% { transform: scale(1); opacity: 0.25; }
50% { transform: scale(1.18); opacity: 0.10; }
0%,
100% {
transform: scale(1);
opacity: 0.25;
}
50% {
transform: scale(1.18);
opacity: 0.1;
}
}
/* ── Texto ───────────────────────────────────────────────── */
.offline-title {
font-size: 1.3rem;
font-weight: 700;
color: var(--text-color, #1a1a2e);
margin: 0 0 10px;
font-size: 1.3rem;
font-weight: 700;
color: var(--text-color, #1a1a2e);
margin: 0 0 10px;
}
.offline-desc {
font-size: 0.88rem;
color: var(--text-color-secondary, #666);
margin: 0 0 28px;
line-height: 1.6;
font-size: 0.88rem;
color: var(--text-color-secondary, #666);
margin: 0 0 28px;
line-height: 1.6;
}
/* ── Dots de pulso ───────────────────────────────────────── */
.offline-pulse-bar {
display: flex;
justify-content: center;
gap: 7px;
display: flex;
justify-content: center;
gap: 7px;
}
.offline-pulse-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--primary-color, #6366f1);
opacity: 0.7;
animation: dot-bounce 1.2s ease-in-out infinite;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--primary-color, #6366f1);
opacity: 0.7;
animation: dot-bounce 1.2s ease-in-out infinite;
}
@keyframes dot-bounce {
0%, 80%, 100% { transform: scale(0.7); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
0%,
80%,
100% {
transform: scale(0.7);
opacity: 0.4;
}
40% {
transform: scale(1);
opacity: 1;
}
}
/* ── Transição do overlay ────────────────────────────────── */
.offline-fade-enter-active { transition: opacity 0.3s ease; }
.offline-fade-leave-active { transition: opacity 0.4s ease; }
.offline-fade-enter-active {
transition: opacity 0.3s ease;
}
.offline-fade-leave-active {
transition: opacity 0.4s ease;
}
.offline-fade-enter-from,
.offline-fade-leave-to { opacity: 0; }
.offline-fade-leave-to {
opacity: 0;
}
/* ── Toast de reconexão ──────────────────────────────────── */
.reconnect-toast {
position: fixed;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
align-items: center;
gap: 8px;
background: #16a34a;
color: #fff;
font-size: 0.875rem;
font-weight: 600;
padding: 10px 20px;
border-radius: 999px;
box-shadow: 0 4px 20px rgba(22, 163, 74, .4);
white-space: nowrap;
position: fixed;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
align-items: center;
gap: 8px;
background: #16a34a;
color: #fff;
font-size: 0.875rem;
font-weight: 600;
padding: 10px 20px;
border-radius: 999px;
box-shadow: 0 4px 20px rgba(22, 163, 74, 0.4);
white-space: nowrap;
}
.reconnect-toast .pi { font-size: 1rem; }
.reconnect-toast .pi {
font-size: 1rem;
}
.reconnect-toast-enter-active { transition: opacity 0.3s ease, transform 0.3s ease; }
.reconnect-toast-leave-active { transition: opacity 0.4s ease, transform 0.4s ease; }
.reconnect-toast-enter-from { opacity: 0; transform: translateX(-50%) translateY(12px); }
.reconnect-toast-leave-to { opacity: 0; transform: translateX(-50%) translateY(12px); }
.reconnect-toast-enter-active {
transition:
opacity 0.3s ease,
transform 0.3s ease;
}
.reconnect-toast-leave-active {
transition:
opacity 0.4s ease,
transform 0.4s ease;
}
.reconnect-toast-enter-from {
opacity: 0;
transform: translateX(-50%) translateY(12px);
}
.reconnect-toast-leave-to {
opacity: 0;
transform: translateX(-50%) translateY(12px);
}
</style>