feat: optimización de imágenes (WebP y AppImage), Lazy Load y LCP (Fase 1 y 2)
This commit is contained in:
88
frontend/src/components/AppImage.vue
Normal file
88
frontend/src/components/AppImage.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="app-image-wrapper" :class="{ 'is-loading': !isLoaded, [wrapperClass]: !!wrapperClass }">
|
||||
<img
|
||||
:src="resolvedSrc"
|
||||
:alt="alt"
|
||||
:loading="priority ? 'eager' : 'lazy'"
|
||||
:fetchpriority="priority ? 'high' : 'auto'"
|
||||
decoding="async"
|
||||
@load="handleLoad"
|
||||
@error="handleError"
|
||||
class="app-image-content"
|
||||
:class="{ 'is-loaded': isLoaded, [imgClass]: !!imgClass }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { getImageUrl } from '@/utils/imageUrl'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src?: string | null;
|
||||
alt?: string;
|
||||
type?: 'taxi' | 'shuttle' | 'business' | 'coupon';
|
||||
priority?: boolean;
|
||||
errorFallback?: string;
|
||||
wrapperClass?: string;
|
||||
imgClass?: string;
|
||||
}>(), {
|
||||
priority: false,
|
||||
wrapperClass: '',
|
||||
imgClass: ''
|
||||
});
|
||||
|
||||
const isLoaded = ref(false)
|
||||
const isError = ref(false)
|
||||
|
||||
const resolvedSrc = computed(() => {
|
||||
if (isError.value && props.errorFallback) return props.errorFallback
|
||||
return getImageUrl(props.src, props.type)
|
||||
})
|
||||
|
||||
function handleLoad() {
|
||||
isLoaded.value = true
|
||||
}
|
||||
|
||||
function handleError(e: Event) {
|
||||
isError.value = true
|
||||
isLoaded.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-image-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
.app-image-wrapper.is-loading {
|
||||
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
.app-image-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
.app-image-content.is-loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.app-image-wrapper.is-loading {
|
||||
background: linear-gradient(90deg, #1f2937 25%, #374151 50%, #1f2937 75%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user