feat: update product page design

This commit is contained in:
2025-12-08 14:05:16 +03:00
parent 5d775e8eb6
commit c64170f2d8

View File

@@ -37,123 +37,160 @@
/> />
<!-- Product info --> <!-- Product info -->
<div class="mx-auto max-w-2xl px-4 pt-3 pb-32 sm:px-6 rounded-t-lg"> <div class="mx-auto max-w-2xl px-4 pt-6 pb-32 sm:px-6">
<div class="lg:col-span-2 lg:border-r lg:pr-8"> <!-- Header section -->
<h1 class="font-bold tracking-tight text-3xl">{{ product.name }}</h1> <div class="mb-6">
<div v-if="product.manufacturer" class="mb-2">
<span class="text-xs font-semibold text-base-content/60 uppercase tracking-wide">{{ product.manufacturer }}</span>
</div>
<h1 class="text-2xl sm:text-3xl font-bold leading-tight mb-4">{{ product.name }}</h1>
<!-- Stock status badge -->
<div class="flex items-center gap-2 mb-4">
<div
class="badge badge-sm"
:class="product.stock === 'В наличии' ? 'badge-success' : 'badge-warning'"
>
{{ product.stock }}
</div>
<span v-if="product.minimum && product.minimum > 1" class="text-xs text-base-content/60">
Мин. заказ: {{ product.minimum }} шт.
</span>
</div> </div>
<div>
<h3 class="text-sm font-medium">{{ product.manufacturer }}</h3>
</div> </div>
<div class="mt-4 lg:row-span-3 lg:mt-0"> <!-- Price section -->
<div v-if="product.special" class="flex items-center"> <div class="card bg-base-200/50 rounded-2xl p-5 mb-6 shadow-sm">
<p class="text-2xl tracking-tight mr-3">{{ product.special }}</p> <div class="flex items-baseline gap-3 flex-wrap">
<p class="text-base-400 line-through">{{ product.price }}</p> <div v-if="product.special" class="flex items-baseline gap-3 flex-wrap">
<span class="text-3xl sm:text-4xl font-bold text-primary">{{ product.special }}</span>
<span class="text-lg text-base-content/50 line-through">{{ product.price }}</span>
</div>
<span v-else class="text-3xl sm:text-4xl font-bold text-primary">{{ product.price }}</span>
</div> </div>
<p v-else class="text-3xl tracking-tight">{{ product.price }}</p>
<p v-if="product.tax" class="text-sm">Без НДС: {{ product.tax }}</p> <div class="mt-3 space-y-1">
<p v-if="product.points && product.points > 0" class="text-sm">Бонусные баллы: {{ product.points }}</p> <p v-if="product.tax" class="text-sm text-base-content/70">
<p v-for="discount in product.discounts" class="text-sm"> <span class="font-medium">Без НДС:</span> {{ product.tax }}
{{ discount.quantity }} или больше {{ discount.price }}
</p> </p>
<p v-if="product.points && product.points > 0" class="text-sm text-base-content/70">
<p v-if="false" class="text-xs">Кол-во на складе: {{ product.quantity }} шт.</p> <span class="font-medium">Бонусные баллы:</span> {{ product.points }}
<p v-if="product.minimum && product.minimum > 1" class="text-xs">
Минимальное кол-во для заказа: {{ product.minimum }}
</p> </p>
<p class="text-xs">Наличие: {{ product.stock }}</p> <div v-if="product.discounts && product.discounts.length > 0" class="mt-2 pt-2 border-t border-base-300">
<p class="text-xs font-semibold text-base-content/60 mb-1">Скидки при покупке:</p>
<p v-for="discount in product.discounts" :key="discount.quantity" class="text-sm text-base-content/70">
{{ discount.quantity }} шт. или больше <span class="font-semibold text-primary">{{ discount.price }}</span>
</p>
</div>
</div>
</div> </div>
<div v-if="product.options && product.options.length" class="mt-4"> <!-- Options section -->
<div v-if="product.options && product.options.length" class="mb-6">
<ProductOptions v-model="product.options"/> <ProductOptions v-model="product.options"/>
</div> </div>
<div class="py-10">
<!-- Description and details --> <!-- Description and details -->
<div>
<h3 class="sr-only">Description</h3>
<div class="space-y-6"> <div class="space-y-6">
<p class="text-base" v-html="product.description"></p> <!-- Description -->
</div> <div v-if="product.description" class="card bg-base-100 rounded-2xl p-5 shadow-sm">
<h3 class="text-lg font-bold mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>
Описание
</h3>
<div class="prose prose-sm max-w-none text-base-content/80" v-html="product.description"></div>
</div> </div>
<div v-if="product.attribute_groups && product.attribute_groups.length > 0" class="mt-3"> <!-- Attributes -->
<h3 class="font-bold mb-2">Характеристики</h3> <div v-if="product.attribute_groups && product.attribute_groups.length > 0" class="card bg-base-100 rounded-2xl p-5 shadow-sm">
<h3 class="text-lg font-bold mb-4 flex items-center gap-2">
<div class="space-y-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<div class="overflow-x-auto"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.593m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h11.25c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z" />
<table class="table table-xs"> </svg>
<tbody> Характеристики
</h3>
<div class="space-y-4">
<template v-for="attrGroup in product.attribute_groups" :key="attrGroup.attribute_group_id"> <template v-for="attrGroup in product.attribute_groups" :key="attrGroup.attribute_group_id">
<tr class="bg-base-200 font-semibold"> <div class="border-b border-base-300 pb-3 last:border-0 last:pb-0">
<td colspan="2">{{ attrGroup.name }}</td> <h4 class="text-sm font-semibold text-base-content/80 mb-2">{{ attrGroup.name }}</h4>
</tr> <div class="space-y-2">
<tr v-for="attr in attrGroup.attribute" :key="attr.attribute_id"> <div v-for="attr in attrGroup.attribute" :key="attr.attribute_id" class="flex justify-between items-start gap-4 py-1">
<td class="w-1/3">{{ attr.name }}</td> <span class="text-sm text-base-content/60 flex-shrink-0">{{ attr.name }}</span>
<td>{{ attr.text }}</td> <span class="text-sm font-medium text-right flex-1">{{ attr.text }}</span>
</tr>
</template>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
</template>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div v-if="product.product_id" <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"> class="fixed px-4 pb-4 pt-4 bottom-0 left-0 w-full bg-base-100/95 backdrop-blur-md z-50 flex flex-col gap-3 border-t border-base-300 shadow-lg"
style="padding-bottom: calc(0.5rem + env(safe-area-inset-bottom));">
<template v-if="settings.store_enabled"> <template v-if="settings.store_enabled">
<div class="text-error"> <div v-if="error" class="alert alert-error alert-sm py-2">
{{ error }} <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-4 w-4" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-xs">{{ error }}</span>
</div> </div>
<div v-if="canAddToCart === false" class="text-error text-center text-xs mt-1"> <div v-if="canAddToCart === false" class="alert alert-warning alert-sm py-2">
Выберите обязательные опции <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-4 w-4" fill="none" viewBox="0 0 24 24">
</div> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="flex gap-2"> <span class="text-xs">Выберите обязательные опции</span>
<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="onCartBtnClick"
>
<span v-if="cart.isLoading" class="loading loading-spinner loading-sm"></span>
{{ btnText }}
</button>
</div> </div>
<div class="flex gap-3 items-center">
<Quantity <Quantity
v-if="isInCart === false" v-if="isInCart === false"
:modelValue="quantity" :modelValue="quantity"
@update:modelValue="setQuantity" @update:modelValue="setQuantity"
size="lg" size="lg"
/> />
<div class="flex-1">
<button
class="btn btn-lg w-full shadow-md"
:class="isInCart ? 'btn-success' : 'btn-primary'"
:disabled="cart.isLoading || canAddToCart === false"
@click="onCartBtnClick"
>
<span v-if="cart.isLoading" class="loading loading-spinner loading-sm"></span>
<template v-else>
<svg v-if="!isInCart" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
</svg>
</template>
{{ btnText }}
</button>
</div>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<button <button
class="btn btn-primary btn-lg w-full" class="btn btn-primary btn-lg w-full shadow-md"
:disabled="! product.share" :disabled="! product.share"
@click="openProductInMarketplace" @click="openProductInMarketplace"
> >
<template v-if="product.share"> <template v-if="product.share">
Открыть товар
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-6"> stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/> d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/>
</svg> </svg>
Открыть товар
</template> </template>
<template v-else>Товар недоступен</template>
<template>Товар недоступен</template>
</button> </button>
</template> </template>
</div> </div>
@@ -393,12 +430,20 @@ onMounted(async () => {
.swiper { .swiper {
height: 500px; height: 500px;
border-radius: var(--radius-box, 0.5rem);
overflow: hidden;
} }
.swiper-slide img { .swiper-slide img {
width: 100%; width: 100%;
height: 500px; height: 500px;
object-fit: contain; object-fit: contain;
cursor: pointer;
transition: transform 0.2s ease;
}
.swiper-slide img:active {
transform: scale(0.98);
} }
.image-preloader { .image-preloader {