Files
interview-demo-code/spa/src/views/Product.vue

266 lines
8.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>