feat: fixed width and preloader for product view page

This commit is contained in:
2025-12-08 13:59:27 +03:00
parent dc198c63b7
commit 5d775e8eb6
3 changed files with 96 additions and 19 deletions

View File

@@ -102,3 +102,4 @@

View File

@@ -1,24 +1,33 @@
<template> <template>
<div> <div>
<div> <div>
<Swiper <div class="relative">
:lazy="true" <div v-if="!firstImageLoaded" class="image-preloader">
:modules="modules" <div class="preloader-spinner"></div>
:pagination="pagination" </div>
@sliderMove="hapticScroll" <Swiper
> v-if="product.images && product.images.length > 0"
<SwiperSlide :lazy="true"
v-for="(image, index) in product.images" :modules="modules"
:key="image.url" :pagination="pagination"
@sliderMove="hapticScroll"
:class="{ 'opacity-0': !firstImageLoaded }"
> >
<img <SwiperSlide
:src="image.thumbnailURL" v-for="(image, index) in product.images"
:alt="image.alt" :key="image.url"
loading="lazy" >
@click="showFullScreen(index)" <img
/> :src="image.thumbnailURL"
</SwiperSlide> :alt="image.alt"
</Swiper> loading="lazy"
@load="onFirstImageLoad"
@error="onFirstImageLoad"
@click="showFullScreen(index)"
/>
</SwiperSlide>
</Swiper>
</div>
<FullScreenImageViewer <FullScreenImageViewer
v-if="isFullScreen" v-if="isFullScreen"
@@ -162,7 +171,7 @@
</template> </template>
<script setup> <script setup>
import {computed, onMounted, onUnmounted, ref} from "vue"; import {computed, nextTick, onMounted, onUnmounted, ref} from "vue";
import {useRoute, useRouter} from 'vue-router'; import {useRoute, useRouter} from 'vue-router';
import ProductOptions from "../components/ProductOptions/ProductOptions.vue"; import ProductOptions from "../components/ProductOptions/ProductOptions.vue";
import {useCartStore} from "../stores/CartStore.js"; import {useCartStore} from "../stores/CartStore.js";
@@ -192,6 +201,7 @@ const initialFullScreenIndex = ref(0);
const isLoading = ref(false); const isLoading = ref(false);
const settings = useSettingsStore(); const settings = useSettingsStore();
const yaMetrika = useYaMetrikaStore(); const yaMetrika = useYaMetrikaStore();
const firstImageLoaded = ref(false);
const canAddToCart = computed(() => { const canAddToCart = computed(() => {
if (!product.value || product.value.options === undefined || product.value.options?.length === 0) { if (!product.value || product.value.options === undefined || product.value.options?.length === 0) {
@@ -280,6 +290,12 @@ function setQuantity(newQuantity) {
window.Telegram.WebApp.HapticFeedback.selectionChanged(); window.Telegram.WebApp.HapticFeedback.selectionChanged();
} }
function onFirstImageLoad() {
if (!firstImageLoaded.value) {
firstImageLoaded.value = true;
}
}
const hapticScroll = useHapticScroll(); const hapticScroll = useHapticScroll();
const pagination = { const pagination = {
clickable: true, clickable: true,
@@ -304,6 +320,27 @@ onMounted(async () => {
try { try {
const {data} = await apiFetch(`/index.php?route=extension/tgshop/handle&api_action=product_show&id=${productId.value}`); const {data} = await apiFetch(`/index.php?route=extension/tgshop/handle&api_action=product_show&id=${productId.value}`);
product.value = data; product.value = data;
// Сброс состояния загрузки первого изображения
firstImageLoaded.value = false;
// Если изображений нет, сразу показываем слайдер
if (!data.images || data.images.length === 0) {
firstImageLoaded.value = true;
} else {
// Проверяем загрузку после следующего тика, чтобы дать Vue время отрендерить изображения
await nextTick();
// Если первое изображение уже загружено из кэша, оно может загрузиться до того, как мы начнем отслеживать
setTimeout(() => {
if (!firstImageLoaded.value) {
// Проверяем, загружено ли первое изображение через проверку complete
const firstImg = document.querySelector('.swiper-slide img');
if (firstImg && firstImg.complete) {
firstImageLoaded.value = true;
}
}
}, 100);
}
window.document.title = data.name; window.document.title = data.name;
yaMetrika.pushHit(route.path, { yaMetrika.pushHit(route.path, {
@@ -353,4 +390,43 @@ onMounted(async () => {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.swiper {
height: 500px;
}
.swiper-slide img {
width: 100%;
height: 500px;
object-fit: contain;
}
.image-preloader {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500px;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--fallback-bc, oklch(var(--bc) / 0.1));
border-radius: var(--radius-box, 0.5rem);
z-index: 10;
}
.preloader-spinner {
width: 48px;
height: 48px;
border: 4px solid var(--fallback-bc, oklch(var(--bc) / 0.2));
border-top-color: var(--fallback-p, oklch(var(--p)));
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style> </style>

View File

@@ -287,7 +287,7 @@ class ProductsService
$images[] = [ $images[] = [
'thumbnailURL' => $this->image 'thumbnailURL' => $this->image
->make($imagePath) ->make($imagePath)
->resize($imageThumbWidth, $imageThumbHeight) ->contain($imageThumbWidth, $imageThumbHeight)
->url(), ->url(),
'largeURL' => $this->image->make($imagePath)->resize($imageFullWidth, $imageFullHeight)->url(), 'largeURL' => $this->image->make($imagePath)->resize($imageFullWidth, $imageFullHeight)->url(),
'width' => $width, 'width' => $width,