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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user