feat: fixed width and preloader for product view page
This commit is contained in:
@@ -102,3 +102,4 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<Swiper
|
||||
:lazy="true"
|
||||
:modules="modules"
|
||||
:pagination="pagination"
|
||||
@sliderMove="hapticScroll"
|
||||
>
|
||||
<SwiperSlide
|
||||
v-for="(image, index) in product.images"
|
||||
:key="image.url"
|
||||
<div class="relative">
|
||||
<div v-if="!firstImageLoaded" class="image-preloader">
|
||||
<div class="preloader-spinner"></div>
|
||||
</div>
|
||||
<Swiper
|
||||
v-if="product.images && product.images.length > 0"
|
||||
:lazy="true"
|
||||
:modules="modules"
|
||||
:pagination="pagination"
|
||||
@sliderMove="hapticScroll"
|
||||
:class="{ 'opacity-0': !firstImageLoaded }"
|
||||
>
|
||||
<img
|
||||
:src="image.thumbnailURL"
|
||||
:alt="image.alt"
|
||||
loading="lazy"
|
||||
@click="showFullScreen(index)"
|
||||
/>
|
||||
</SwiperSlide>
|
||||
</Swiper>
|
||||
<SwiperSlide
|
||||
v-for="(image, index) in product.images"
|
||||
:key="image.url"
|
||||
>
|
||||
<img
|
||||
:src="image.thumbnailURL"
|
||||
:alt="image.alt"
|
||||
loading="lazy"
|
||||
@load="onFirstImageLoad"
|
||||
@error="onFirstImageLoad"
|
||||
@click="showFullScreen(index)"
|
||||
/>
|
||||
</SwiperSlide>
|
||||
</Swiper>
|
||||
</div>
|
||||
|
||||
<FullScreenImageViewer
|
||||
v-if="isFullScreen"
|
||||
@@ -162,7 +171,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onMounted, onUnmounted, ref} from "vue";
|
||||
import {computed, nextTick, onMounted, onUnmounted, ref} from "vue";
|
||||
import {useRoute, useRouter} from 'vue-router';
|
||||
import ProductOptions from "../components/ProductOptions/ProductOptions.vue";
|
||||
import {useCartStore} from "../stores/CartStore.js";
|
||||
@@ -192,6 +201,7 @@ const initialFullScreenIndex = ref(0);
|
||||
const isLoading = ref(false);
|
||||
const settings = useSettingsStore();
|
||||
const yaMetrika = useYaMetrikaStore();
|
||||
const firstImageLoaded = ref(false);
|
||||
|
||||
const canAddToCart = computed(() => {
|
||||
if (!product.value || product.value.options === undefined || product.value.options?.length === 0) {
|
||||
@@ -280,6 +290,12 @@ function setQuantity(newQuantity) {
|
||||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||||
}
|
||||
|
||||
function onFirstImageLoad() {
|
||||
if (!firstImageLoaded.value) {
|
||||
firstImageLoaded.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
const hapticScroll = useHapticScroll();
|
||||
const pagination = {
|
||||
clickable: true,
|
||||
@@ -304,6 +320,27 @@ onMounted(async () => {
|
||||
try {
|
||||
const {data} = await apiFetch(`/index.php?route=extension/tgshop/handle&api_action=product_show&id=${productId.value}`);
|
||||
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;
|
||||
|
||||
yaMetrika.pushHit(route.path, {
|
||||
@@ -353,4 +390,43 @@ onMounted(async () => {
|
||||
justify-content: 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>
|
||||
|
||||
@@ -287,7 +287,7 @@ class ProductsService
|
||||
$images[] = [
|
||||
'thumbnailURL' => $this->image
|
||||
->make($imagePath)
|
||||
->resize($imageThumbWidth, $imageThumbHeight)
|
||||
->contain($imageThumbWidth, $imageThumbHeight)
|
||||
->url(),
|
||||
'largeURL' => $this->image->make($imagePath)->resize($imageFullWidth, $imageFullHeight)->url(),
|
||||
'width' => $width,
|
||||
|
||||
Reference in New Issue
Block a user