carousel, agenda arquivados, agenda cor, agenda arquivados, grupos pacientes, pacientes arquivados - desativados, sessoes verificadas, ajuste notificações, Prontuario, Agenda Animation, Menu Profile, bagdes Profile, Offline
This commit is contained in:
245
src/components/AppOfflineOverlay.vue
Normal file
245
src/components/AppOfflineOverlay.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<!-- src/components/AppOfflineOverlay.vue -->
|
||||
<!-- Detecta offline via eventos nativos do browser + polling de fetch -->
|
||||
<script setup>
|
||||
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)
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ── Eventos nativos do browser ────────────────────────────────
|
||||
function onBrowserOffline () { setOffline() }
|
||||
function onBrowserOnline () { checkConnectivity() } // confirma antes de marcar online
|
||||
|
||||
onMounted(() => {
|
||||
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)
|
||||
|
||||
// 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)
|
||||
})
|
||||
</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" />
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- ── 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;
|
||||
}
|
||||
|
||||
.offline-backdrop {
|
||||
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);
|
||||
}
|
||||
|
||||
/* ── Ícone ───────────────────────────────────────────────── */
|
||||
.offline-icon-wrap {
|
||||
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;
|
||||
}
|
||||
|
||||
.offline-icon {
|
||||
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; }
|
||||
}
|
||||
|
||||
/* ── Texto ───────────────────────────────────────────────── */
|
||||
.offline-title {
|
||||
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;
|
||||
}
|
||||
|
||||
/* ── Dots de pulso ───────────────────────────────────────── */
|
||||
.offline-pulse-bar {
|
||||
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;
|
||||
}
|
||||
|
||||
@keyframes dot-bounce {
|
||||
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-from,
|
||||
.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;
|
||||
}
|
||||
|
||||
.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); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user