fix: resolve blank screen on return from external links
This commit is contained in:
@ -21,6 +21,15 @@ const isAuthScreen = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let lastHiddenAt: number | null = null
|
let lastHiddenAt: number | null = null
|
||||||
|
let refocusDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
function dispatchRefocus(reason: string) {
|
||||||
|
// Debounce: no disparar dos eventos seguidos en menos de 1 segundo
|
||||||
|
if (refocusDebounceTimer) return
|
||||||
|
refocusDebounceTimer = setTimeout(() => { refocusDebounceTimer = null }, 1000)
|
||||||
|
console.log(`SIBU | App refocus — motivo: ${reason}`)
|
||||||
|
window.dispatchEvent(new CustomEvent('app-refocus'))
|
||||||
|
}
|
||||||
|
|
||||||
function handleVisibilityChange() {
|
function handleVisibilityChange() {
|
||||||
if (document.visibilityState === 'hidden') {
|
if (document.visibilityState === 'hidden') {
|
||||||
@ -29,10 +38,32 @@ function handleVisibilityChange() {
|
|||||||
}
|
}
|
||||||
if (document.visibilityState === 'visible' && lastHiddenAt !== null) {
|
if (document.visibilityState === 'visible' && lastHiddenAt !== null) {
|
||||||
const secondsAway = (Date.now() - lastHiddenAt) / 1000
|
const secondsAway = (Date.now() - lastHiddenAt) / 1000
|
||||||
// Si pasaron más de 3 segundos, asumimos que el proceso pudo ser suspendido
|
// Umbral bajo (1 s) para capturar retornos rápidos desde Google Maps / links externos
|
||||||
if (secondsAway > 3) {
|
if (secondsAway > 1) {
|
||||||
console.log(`SIBU | App recuperada tras ${secondsAway.toFixed(1)}s. Disparando refocus...`)
|
dispatchRefocus(`visibilitychange tras ${secondsAway.toFixed(1)}s`)
|
||||||
window.dispatchEvent(new CustomEvent('app-refocus'))
|
}
|
||||||
|
lastHiddenAt = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: 'pageshow' se dispara al volver desde el bfcache (back-forward cache)
|
||||||
|
// Muy común en Safari/iOS cuando se abre un link externo y se regresa
|
||||||
|
function handlePageShow(event: PageTransitionEvent) {
|
||||||
|
if (event.persisted) {
|
||||||
|
// La página fue restaurada desde bfcache — siempre necesita refocus
|
||||||
|
dispatchRefocus('pageshow persisted (bfcache)')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: 'focus' en window cubre casos donde visibilitychange no se dispara
|
||||||
|
// (algunos navegadores Android al volver de otra app)
|
||||||
|
let windowFocusTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
function handleWindowFocus() {
|
||||||
|
// Solo si la pestaña estuvo oculta antes
|
||||||
|
if (lastHiddenAt !== null) {
|
||||||
|
const secondsAway = (Date.now() - lastHiddenAt) / 1000
|
||||||
|
if (secondsAway > 1) {
|
||||||
|
dispatchRefocus(`window focus tras ${secondsAway.toFixed(1)}s`)
|
||||||
}
|
}
|
||||||
lastHiddenAt = null
|
lastHiddenAt = null
|
||||||
}
|
}
|
||||||
@ -49,10 +80,16 @@ onMounted(() => {
|
|||||||
favoritesStore.loadFavorites()
|
favoritesStore.loadFavorites()
|
||||||
}
|
}
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
window.addEventListener('pageshow', handlePageShow)
|
||||||
|
window.addEventListener('focus', handleWindowFocus)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
window.removeEventListener('pageshow', handlePageShow)
|
||||||
|
window.removeEventListener('focus', handleWindowFocus)
|
||||||
|
if (refocusDebounceTimer) clearTimeout(refocusDebounceTimer)
|
||||||
|
if (windowFocusTimer) clearTimeout(windowFocusTimer)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -26,12 +26,25 @@ export const useShuttleStore = defineStore('shuttle', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recarga silenciosa: no activa el spinner (para refocus desde background)
|
||||||
|
async function silentReload() {
|
||||||
|
if (shuttles.value.length === 0) { return loadShuttles() }
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
shuttles.value = await shuttlesService.getAllShuttles(filters.value)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error silent-reloading shuttles:', e)
|
||||||
|
// No mostrar error si ya hay datos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shuttles,
|
shuttles,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
filters,
|
filters,
|
||||||
loadShuttles,
|
loadShuttles,
|
||||||
|
silentReload,
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
persist: {
|
persist: {
|
||||||
|
|||||||
@ -31,6 +31,18 @@ export const useTaxiStore = defineStore('taxi', () => {
|
|||||||
loadTaxis()
|
loadTaxis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recarga silenciosa: no activa el spinner (para refocus desde background)
|
||||||
|
async function silentReload() {
|
||||||
|
if (taxis.value.length === 0) { return loadTaxis() }
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
taxis.value = await taxisService.getAllTaxis(filters.value)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error silent-reloading taxis:', e)
|
||||||
|
// No mostrar error si ya hay datos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
taxis,
|
taxis,
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -38,6 +50,7 @@ export const useTaxiStore = defineStore('taxi', () => {
|
|||||||
filters,
|
filters,
|
||||||
loadTaxis,
|
loadTaxis,
|
||||||
setFilters,
|
setFilters,
|
||||||
|
silentReload,
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
persist: {
|
persist: {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import AuthGuard from '@/components/common/AuthGuard.vue'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const businesses = ref<Business[]>([])
|
const businesses = ref<Business[]>([])
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const selectedCategory = ref('Todas')
|
const selectedCategory = ref('Todas')
|
||||||
@ -41,21 +41,28 @@ function catIcon(cat: string) {
|
|||||||
return CATEGORY_META[cat]?.icon ?? 'place'
|
return CATEGORY_META[cat]?.icon ?? 'place'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadBusinesses() {
|
async function loadBusinesses(silent = false) {
|
||||||
isLoading.value = true
|
// Modo 'silencioso': si ya tenemos datos, no mostrar spinner — solo refrescar en fondo
|
||||||
|
if (!silent || businesses.value.length === 0) {
|
||||||
|
isLoading.value = true
|
||||||
|
}
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
businesses.value = await businessService.getAllBusinesses()
|
businesses.value = await businessService.getAllBusinesses()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error loading businesses:', e)
|
console.error('Error loading businesses:', e)
|
||||||
error.value = t('discover.error')
|
// Solo mostrar error si no hay datos previos
|
||||||
|
if (businesses.value.length === 0) {
|
||||||
|
error.value = t('discover.error')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRefocus() {
|
function handleRefocus() {
|
||||||
loadBusinesses()
|
// Recarga silenciosa: no congela la UI si ya hay datos visibles
|
||||||
|
loadBusinesses(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -179,7 +186,7 @@ function resetFilters() {
|
|||||||
<div v-else-if="error" class="state-center">
|
<div v-else-if="error" class="state-center">
|
||||||
<span class="material-icons" style="font-size: 3.5rem; color: #ef4444; opacity: 0.8; margin-bottom: 0.5rem;">error_outline</span>
|
<span class="material-icons" style="font-size: 3.5rem; color: #ef4444; opacity: 0.8; margin-bottom: 0.5rem;">error_outline</span>
|
||||||
<p style="font-weight: 600; color: var(--text-secondary);">{{ error }}</p>
|
<p style="font-weight: 600; color: var(--text-secondary);">{{ error }}</p>
|
||||||
<button class="cta-btn" style="margin-top: 1rem;" @click="loadBusinesses">
|
<button class="cta-btn" style="margin-top: 1rem;" @click="loadBusinesses()">
|
||||||
<span class="material-icons">refresh</span>
|
<span class="material-icons">refresh</span>
|
||||||
{{ t('common.retry') }}
|
{{ t('common.retry') }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -143,10 +143,28 @@ async function fetchData() {
|
|||||||
updateActiveUnits();
|
updateActiveUnits();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRefocus() {
|
async function handleRefocus() {
|
||||||
|
// Refrescar datos en fondo
|
||||||
fetchData();
|
fetchData();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
if (map.value) {
|
if (map.value) {
|
||||||
google.maps.event.trigger(map.value, 'resize');
|
// El mapa sigue vivo — solo redimensionar y actualizar
|
||||||
|
try {
|
||||||
|
google.maps.event.trigger(map.value, 'resize');
|
||||||
|
} catch (_) { /* ignorar si google no disponible */ }
|
||||||
|
updateActiveUnits();
|
||||||
|
} else {
|
||||||
|
// El mapa fue destruido por el browser al suspender la pestaña — reinicializar
|
||||||
|
console.log('SIBU | Mapa perdido tras refocus, reinicializando...');
|
||||||
|
if (isLoaded.value) {
|
||||||
|
await initializeMap();
|
||||||
|
} else {
|
||||||
|
const unwatch = watch(isLoaded, async (loaded) => {
|
||||||
|
if (loaded) { await initializeMap(); unwatch(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +197,6 @@ onMounted(async () => {
|
|||||||
unitFetchInterval.value = setInterval(updateActiveUnits, 15000);
|
unitFetchInterval.value = setInterval(updateActiveUnits, 15000);
|
||||||
startCarousel();
|
startCarousel();
|
||||||
|
|
||||||
// 🛰️ RESIZE FIX: Trigger map resize when app becomes visible again
|
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -192,8 +209,9 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
function handleVisibilityChange() {
|
function handleVisibilityChange() {
|
||||||
if (document.visibilityState === 'visible' && map.value) {
|
if (document.visibilityState === 'visible' && map.value) {
|
||||||
console.log('SIBU | App visible, redimensionando mapa...');
|
try {
|
||||||
google.maps.event.trigger(map.value, 'resize');
|
google.maps.event.trigger(map.value, 'resize');
|
||||||
|
} catch (_) { /* ignorar */ }
|
||||||
updateActiveUnits();
|
updateActiveUnits();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,8 @@ function fetchData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleRefocus() {
|
function handleRefocus() {
|
||||||
fetchData()
|
// Recarga silenciosa: no congela la UI si ya hay datos
|
||||||
|
taxiStore.silentReload()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@ -34,7 +34,8 @@ function fetchData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleRefocus() {
|
function handleRefocus() {
|
||||||
fetchData()
|
// Recarga silenciosa: no congela la UI si ya hay datos
|
||||||
|
shuttleStore.silentReload()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user