266 lines
8.2 KiB
Vue
266 lines
8.2 KiB
Vue
<template>
|
||
<div class="safe-top">
|
||
<div>
|
||
<swiper-container ref="swiperEl" init="false">
|
||
<swiper-slide
|
||
v-for="(image, index) in product.images"
|
||
lazy="true"
|
||
>
|
||
<img
|
||
:src="image.thumbnailURL"
|
||
:alt="image.alt"
|
||
@click="showFullScreen(index)"
|
||
/>
|
||
</swiper-slide>
|
||
</swiper-container>
|
||
|
||
<FullScreenImageViewer
|
||
v-if="isFullScreen"
|
||
:images="product.images"
|
||
:activeIndex="initialFullScreenIndex"
|
||
@close="closeFullScreen"
|
||
/>
|
||
|
||
<!-- Product info -->
|
||
<div class="mx-auto max-w-2xl px-4 pt-3 pb-32 sm:px-6 rounded-t-lg">
|
||
<div class="lg:col-span-2 lg:border-r lg:pr-8">
|
||
<h1 class="font-bold tracking-tight text-3xl">{{ product.name }}</h1>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-sm font-medium">{{ product.manufacturer }}</h3>
|
||
</div>
|
||
|
||
<div class="mt-4 lg:row-span-3 lg:mt-0">
|
||
<div v-if="product.special" class="flex items-center">
|
||
<p class="text-2xl tracking-tight mr-3">{{ product.special }}</p>
|
||
<p class="text-base-400 line-through">{{ product.price }}</p>
|
||
</div>
|
||
<p v-else class="text-3xl tracking-tight">{{ product.price }}</p>
|
||
|
||
<p v-if="product.tax" class="text-sm">Без НДС: {{ product.tax }}</p>
|
||
<p v-if="product.points && product.points > 0" class="text-sm">Бонусные баллы: {{ product.points }}</p>
|
||
<p v-for="discount in product.discounts" class="text-sm">
|
||
{{ discount.quantity }} или больше {{ discount.price }}
|
||
</p>
|
||
|
||
<p v-if="false" class="text-xs">Кол-во на складе: {{ product.quantity }} шт.</p>
|
||
<p v-if="product.minimum && product.minimum > 1">Минимальное кол-во для заказа: {{ product.minimum }}</p>
|
||
</div>
|
||
|
||
<div class="badge badge-primary">{{ product.stock }}</div>
|
||
|
||
<div v-if="product.options && product.options.length" class="mt-4">
|
||
<ProductOptions v-model="product.options"/>
|
||
</div>
|
||
|
||
<div class="py-10">
|
||
<!-- Description and details -->
|
||
<div>
|
||
<h3 class="sr-only">Description</h3>
|
||
|
||
<div class="space-y-6">
|
||
<p class="text-base" v-html="product.description"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="product.attribute_groups && product.attribute_groups.length > 0" class="mt-3">
|
||
<h3 class="font-bold mb-2">Характеристики</h3>
|
||
|
||
<div class="space-y-6">
|
||
<div class="overflow-x-auto">
|
||
<table class="table table-xs">
|
||
<tbody>
|
||
<template v-for="attrGroup in product.attribute_groups" :key="attrGroup.attribute_group_id">
|
||
<tr class="bg-base-200 font-semibold">
|
||
<td colspan="2">{{ attrGroup.name }}</td>
|
||
</tr>
|
||
<tr v-for="attr in attrGroup.attribute" :key="attr.attribute_id">
|
||
<td class="w-1/3">{{ attr.name }}</td>
|
||
<td>{{ attr.text }}</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="product.product_id" class="fixed px-4 pb-10 pt-4 bottom-0 left-0 w-full bg-base-200 z-50 flex flex-col gap-2 border-t-1 border-t-base-300">
|
||
<div class="text-error">
|
||
{{ error }}
|
||
</div>
|
||
|
||
<div v-if="canAddToCart === false" class="text-error text-center text-xs mt-1">
|
||
Выберите обязательные опции
|
||
</div>
|
||
|
||
<div class="flex gap-2">
|
||
<div class="flex-1">
|
||
<button
|
||
class="btn btn-primary btn-lg w-full"
|
||
:class="isInCart ? 'btn-success' : 'btn-primary'"
|
||
:disabled="cart.isLoading || canAddToCart === false"
|
||
@click="actionBtnClick"
|
||
>
|
||
<span v-if="cart.isLoading" class="loading loading-spinner loading-sm"></span>
|
||
{{ btnText }}
|
||
</button>
|
||
</div>
|
||
|
||
<Quantity
|
||
v-if="isInCart === false"
|
||
:modelValue="quantity"
|
||
@update:modelValue="setQuantity"
|
||
size="lg"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<ProductNotFound v-else/>
|
||
|
||
<FullScreenImageViewer
|
||
v-if="isFullScreen"
|
||
:images="product.images"
|
||
:activeIndex="initialFullScreenIndex"
|
||
@close="closeFullScreen"
|
||
/>
|
||
<LoadingFullScreen v-if="isLoading"/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {computed, onMounted, onUnmounted, ref} from "vue";
|
||
import {useRoute, useRouter} from 'vue-router'
|
||
import ProductOptions from "../components/ProductOptions/ProductOptions.vue";
|
||
import {useCartStore} from "../stores/CartStore.js";
|
||
import Quantity from "../components/Quantity.vue";
|
||
import {SUPPORTED_OPTION_TYPES} from "@/constants/options.js";
|
||
import {apiFetch} from "@/utils/ftch.js";
|
||
import FullScreenImageViewer from "@/components/FullScreenImageViewer.vue";
|
||
import LoadingFullScreen from "@/components/LoadingFullScreen.vue";
|
||
import ProductNotFound from "@/components/ProductNotFound.vue";
|
||
|
||
const route = useRoute();
|
||
const productId = computed(() => route.params.id);
|
||
const product = ref({});
|
||
const cart = useCartStore();
|
||
const quantity = ref(1);
|
||
const error = ref('');
|
||
const router = useRouter();
|
||
const isInCart = ref(false);
|
||
const btnText = computed(() => isInCart.value ? 'В корзине' : 'Купить');
|
||
const isFullScreen = ref(false);
|
||
const initialFullScreenIndex = ref(0);
|
||
const isLoading = ref(false);
|
||
|
||
const canAddToCart = computed(() => {
|
||
if (!product.value || product.value.options === undefined || product.value.options?.length === 0) {
|
||
return true;
|
||
}
|
||
|
||
const required = product.value.options.filter(item => {
|
||
return SUPPORTED_OPTION_TYPES.includes(item.type)
|
||
&& item.required === true
|
||
&& !item.value;
|
||
});
|
||
|
||
return required.length === 0;
|
||
});
|
||
|
||
function showFullScreen(index) {
|
||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||
isFullScreen.value = true;
|
||
initialFullScreenIndex.value = index;
|
||
document.body.style.overflow = 'hidden';
|
||
history.pushState({ fullscreen: true }, '');
|
||
}
|
||
|
||
function closeFullScreen() {
|
||
isFullScreen.value = false;
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
async function actionBtnClick() {
|
||
try {
|
||
error.value = '';
|
||
|
||
if (isInCart.value === false) {
|
||
await cart.addProduct(productId.value, product.value.name, product.value.price, quantity.value, product.value.options);
|
||
isInCart.value = true;
|
||
window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
|
||
} else {
|
||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||
await router.push({'name': 'cart'});
|
||
}
|
||
|
||
} catch (e) {
|
||
await window.Telegram.WebApp.HapticFeedback.notificationOccurred('error');
|
||
error.value = e.message;
|
||
}
|
||
}
|
||
|
||
function setQuantity(newQuantity) {
|
||
quantity.value = newQuantity;
|
||
window.Telegram.WebApp.HapticFeedback.selectionChanged();
|
||
}
|
||
|
||
let canVibrate = true;
|
||
|
||
function onPopState() {
|
||
if (isFullScreen.value) {
|
||
closeFullScreen();
|
||
} else {
|
||
// пусть Vue Router сам обработает
|
||
router.back();
|
||
}
|
||
}
|
||
|
||
const swiperEl = ref(null);
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('popstate', onPopState);
|
||
});
|
||
|
||
onMounted(async () => {
|
||
isLoading.value = true;
|
||
try {
|
||
const {data} = await apiFetch(`/index.php?route=extension/tgshop/handle&api_action=product_show&id=${productId.value}`);
|
||
product.value = data;
|
||
} catch (error) {
|
||
console.error(error);
|
||
} finally {
|
||
isLoading.value = false;
|
||
}
|
||
|
||
|
||
window.addEventListener('popstate', onPopState);
|
||
|
||
swiperEl.value.addEventListener('swiperslidermove', (event) => {
|
||
if (!canVibrate) return;
|
||
window.Telegram.WebApp.HapticFeedback.impactOccurred('soft');
|
||
canVibrate = false;
|
||
setTimeout(() => {
|
||
canVibrate = true;
|
||
}, 50);
|
||
});
|
||
|
||
Object.assign(swiperEl.value, {
|
||
injectStyles: [`
|
||
.swiper-pagination {
|
||
position: relative;
|
||
padding-top: 15px;
|
||
}
|
||
`],
|
||
pagination: {
|
||
dynamicBullets: true,
|
||
clickable: true,
|
||
},
|
||
});
|
||
|
||
swiperEl.value.initialize();
|
||
});
|
||
</script>
|