Refine catalog detail layout
This commit is contained in:
@@ -263,6 +263,21 @@ function visibleFields(group: ProductGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedGroup = computed(() => productGroups.value.find((group) => group.key === props.productTypeSlug) ?? null);
|
const selectedGroup = computed(() => productGroups.value.find((group) => group.key === props.productTypeSlug) ?? null);
|
||||||
|
const currentGroupIndex = computed(() => productGroups.value.findIndex((group) => group.key === props.productTypeSlug));
|
||||||
|
const previousGroup = computed(() => {
|
||||||
|
if (currentGroupIndex.value <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return productGroups.value[currentGroupIndex.value - 1] ?? null;
|
||||||
|
});
|
||||||
|
const nextGroup = computed(() => {
|
||||||
|
if (currentGroupIndex.value < 0 || currentGroupIndex.value >= productGroups.value.length - 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return productGroups.value[currentGroupIndex.value + 1] ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
function requiredKeys(group: ProductGroup) {
|
function requiredKeys(group: ProductGroup) {
|
||||||
return visibleFields(group).map((field) => field.key);
|
return visibleFields(group).map((field) => field.key);
|
||||||
@@ -564,177 +579,206 @@ function decrementSelected(group: ProductGroup) {
|
|||||||
decrementProduct(product.id);
|
decrementProduct(product.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function productDetailPath(group: ProductGroup) {
|
||||||
|
return `/products/${group.key}`;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="space-y-5">
|
<section class="space-y-5">
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<NuxtLink to="/products" class="btn btn-ghost rounded-full px-3">
|
|
||||||
← Назад к каталогу
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="loading" class="alert surface-card border-0">Загрузка каталога...</div>
|
<div v-if="loading" class="alert surface-card border-0">Загрузка каталога...</div>
|
||||||
<div v-else-if="error" class="alert alert-error">{{ error.message }}</div>
|
<div v-else-if="error" class="alert alert-error">{{ error.message }}</div>
|
||||||
<div v-else-if="selectedGroup" class="space-y-4">
|
<div v-else-if="selectedGroup" class="space-y-4">
|
||||||
<article class="surface-card rounded-3xl p-4 md:p-5">
|
<NuxtLink
|
||||||
<div class="grid gap-4 xl:grid-cols-6 xl:items-start">
|
v-if="previousGroup"
|
||||||
<div class="p-3 xl:col-span-1">
|
:to="productDetailPath(previousGroup)"
|
||||||
<img
|
class="fixed left-6 top-1/2 z-10 hidden h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full border border-[#dce9e1] bg-white text-xl text-[#163624] shadow-[0_12px_28px_rgba(18,56,36,0.08)] 2xl:flex"
|
||||||
:src="createProductCover(selectedGroup.typeLabel, selectedGroup.key)"
|
aria-label="Предыдущий товар"
|
||||||
:alt="`Превью группы ${selectedGroup.typeLabel}`"
|
>
|
||||||
class="aspect-square w-full rounded-[24px] object-cover"
|
←
|
||||||
loading="lazy"
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="nextGroup"
|
||||||
|
:to="productDetailPath(nextGroup)"
|
||||||
|
class="fixed right-6 top-1/2 z-10 hidden h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full border border-[#dce9e1] bg-white text-xl text-[#163624] shadow-[0_12px_28px_rgba(18,56,36,0.08)] 2xl:flex"
|
||||||
|
aria-label="Следующий товар"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<NuxtLink to="/products" class="btn btn-ghost rounded-full px-3 text-[#163624]">
|
||||||
|
← Назад
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<NuxtLink
|
||||||
|
v-if="previousGroup"
|
||||||
|
:to="productDetailPath(previousGroup)"
|
||||||
|
class="btn btn-ghost btn-circle"
|
||||||
|
aria-label="Предыдущий товар"
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="nextGroup"
|
||||||
|
:to="productDetailPath(nextGroup)"
|
||||||
|
class="btn btn-ghost btn-circle"
|
||||||
|
aria-label="Следующий товар"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="rounded-[28px] border border-[#e6efe9] bg-white p-5 md:p-7">
|
||||||
|
<h1 class="text-3xl font-bold text-[#163624]">{{ selectedGroup.typeLabel }}</h1>
|
||||||
|
|
||||||
|
<div v-if="customizationDetails(selectedGroup).length" class="mt-4 flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="note in customizationDetails(selectedGroup)"
|
||||||
|
:key="`${selectedGroup.key}-${note}`"
|
||||||
|
class="rounded-full border border-[#dce9e1] bg-white px-3 py-1 text-xs font-semibold text-[#355947]"
|
||||||
>
|
>
|
||||||
|
{{ note }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="p-4 md:p-5 xl:col-span-4">
|
<div class="space-y-3">
|
||||||
<div class="mb-4">
|
<article
|
||||||
<h2 class="text-2xl font-bold text-[#163624]">{{ selectedGroup.typeLabel }}</h2>
|
v-for="field in visibleFields(selectedGroup)"
|
||||||
<p class="mt-2 max-w-3xl text-sm leading-6 text-[#5d7468]">
|
:key="`${selectedGroup.key}-${field.key}`"
|
||||||
Выберите параметры внутри карточки, сразу посмотрите ограничения и ниже изучите полный список вариантов по этой позиции.
|
class="rounded-[24px] border border-[#e6efe9] bg-white p-5"
|
||||||
</p>
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
<div v-if="customizationDetails(selectedGroup).length" class="mt-4 rounded-[24px] bg-[#eef8f1] p-4">
|
<div class="space-y-2">
|
||||||
<p class="text-xs font-bold uppercase tracking-[0.12em] text-[#15613d]">Под заказ</p>
|
<h2 class="text-lg font-semibold text-[#163624]">{{ field.label }}</h2>
|
||||||
<div class="mt-3 flex flex-wrap gap-2">
|
<p class="text-sm leading-6 text-[#5d7468]">{{ fieldHelperText(selectedGroup, field.key) }}</p>
|
||||||
<span
|
|
||||||
v-for="note in customizationDetails(selectedGroup)"
|
|
||||||
:key="`${selectedGroup.key}-${note}`"
|
|
||||||
class="rounded-full bg-white px-3 py-1 text-xs font-semibold text-[#15613d]"
|
|
||||||
>
|
|
||||||
{{ note }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="flex flex-wrap gap-2">
|
||||||
<div
|
<label
|
||||||
v-for="field in visibleFields(selectedGroup)"
|
v-for="option in getAllFieldOptions(selectedGroup, field.key)"
|
||||||
:key="`${selectedGroup.key}-${field.key}`"
|
:key="`${selectedGroup.key}-${field.key}-${option}`"
|
||||||
class="rounded-[22px] border border-base-200 bg-[#fbfcfb] p-4"
|
class="btn btn-sm rounded-full text-sm normal-case shadow-none transition"
|
||||||
>
|
:class="[
|
||||||
<div class="grid gap-4 xl:grid-cols-[minmax(0,1fr)_260px] xl:items-start">
|
getGroupState(selectedGroup)[field.key] === option
|
||||||
<div>
|
? 'bg-neutral text-neutral-content'
|
||||||
<p class="text-sm font-semibold text-[#163624]">{{ field.label }}</p>
|
: isOptionAvailable(selectedGroup, field.key, option)
|
||||||
|
? 'bg-base-100 text-base-content hover:bg-base-200'
|
||||||
<div class="mt-3 flex flex-wrap gap-2">
|
: 'bg-[#eef1f4] text-[#7b8591] hover:bg-[#e3e7eb]',
|
||||||
<label
|
'cursor-pointer',
|
||||||
v-for="option in getAllFieldOptions(selectedGroup, field.key)"
|
]"
|
||||||
:key="`${selectedGroup.key}-${field.key}-${option}`"
|
|
||||||
class="btn btn-sm rounded-full text-sm normal-case shadow-none transition"
|
|
||||||
:class="[
|
|
||||||
getGroupState(selectedGroup)[field.key] === option
|
|
||||||
? 'bg-neutral text-neutral-content'
|
|
||||||
: isOptionAvailable(selectedGroup, field.key, option)
|
|
||||||
? 'bg-base-100 text-base-content hover:bg-base-200'
|
|
||||||
: 'bg-[#eef1f4] text-[#7b8591] hover:bg-[#e3e7eb]',
|
|
||||||
'cursor-pointer',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
class="sr-only"
|
|
||||||
:name="`${selectedGroup.key}-${field.key}`"
|
|
||||||
:checked="getGroupState(selectedGroup)[field.key] === option"
|
|
||||||
@change="updateField(selectedGroup, field.key, option)"
|
|
||||||
>
|
|
||||||
<span>{{ formatOptionLabel(field.key, option) }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-[18px] bg-white p-3 text-sm leading-6 text-[#5d7468]">
|
|
||||||
{{ fieldHelperText(selectedGroup, field.key) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<aside class="p-4 md:p-5 xl:col-span-1">
|
|
||||||
<div class="flex h-full flex-col justify-between gap-4">
|
|
||||||
<div />
|
|
||||||
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="rounded-[22px] bg-[#f5faf7] px-4 py-3 text-center text-sm font-medium text-[#4c6a5a]">
|
|
||||||
Артикул: <span class="font-semibold text-[#163624]">{{ articleLabel(selectedGroup) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="selectedQty(selectedGroup) === 0"
|
|
||||||
class="btn h-11 w-full rounded-full border-0 bg-[#139957] text-sm font-semibold text-white hover:bg-[#0d854a]"
|
|
||||||
:disabled="!selectedProduct(selectedGroup)"
|
|
||||||
@click="incrementSelected(selectedGroup)"
|
|
||||||
>
|
>
|
||||||
В корзину
|
<input
|
||||||
</button>
|
type="radio"
|
||||||
|
class="sr-only"
|
||||||
|
:name="`${selectedGroup.key}-${field.key}`"
|
||||||
|
:checked="getGroupState(selectedGroup)[field.key] === option"
|
||||||
|
@change="updateField(selectedGroup, field.key, option)"
|
||||||
|
>
|
||||||
|
<span>{{ formatOptionLabel(field.key, option) }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
<div v-else class="rounded-[22px] border border-base-300 bg-base-100 px-2 py-1">
|
<article class="rounded-[24px] border border-[#e6efe9] bg-white p-5">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="space-y-4">
|
||||||
<button
|
<div class="space-y-1">
|
||||||
class="btn btn-square btn-sm"
|
<h2 class="text-lg font-semibold text-[#163624]">В корзину</h2>
|
||||||
:disabled="selectedQty(selectedGroup) === 0"
|
<p class="text-sm leading-6 text-[#5d7468]">
|
||||||
@click="decrementSelected(selectedGroup)"
|
Артикул: <span class="font-semibold text-[#163624]">{{ articleLabel(selectedGroup) }}</span>
|
||||||
>
|
</p>
|
||||||
-
|
</div>
|
||||||
</button>
|
|
||||||
<div class="min-w-10 text-center text-lg font-semibold text-[#163624]">{{ selectedQty(selectedGroup) }}</div>
|
<button
|
||||||
<button class="btn btn-square btn-sm" :disabled="!selectedProduct(selectedGroup)" @click="incrementSelected(selectedGroup)">
|
v-if="selectedQty(selectedGroup) === 0"
|
||||||
+
|
class="btn h-11 rounded-full border-0 bg-[#139957] px-6 text-sm font-semibold text-white hover:bg-[#0d854a]"
|
||||||
</button>
|
:disabled="!selectedProduct(selectedGroup)"
|
||||||
</div>
|
@click="incrementSelected(selectedGroup)"
|
||||||
|
>
|
||||||
|
В корзину
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-else class="inline-flex rounded-[22px] border border-base-300 bg-base-100 px-2 py-1">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-square btn-sm"
|
||||||
|
:disabled="selectedQty(selectedGroup) === 0"
|
||||||
|
@click="decrementSelected(selectedGroup)"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<div class="min-w-10 text-center text-lg font-semibold text-[#163624]">{{ selectedQty(selectedGroup) }}</div>
|
||||||
|
<button class="btn btn-square btn-sm" :disabled="!selectedProduct(selectedGroup)" @click="incrementSelected(selectedGroup)">
|
||||||
|
+
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</article>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 overflow-x-auto rounded-[28px] bg-white">
|
<article class="rounded-[24px] border border-[#e6efe9] bg-white p-5">
|
||||||
<table class="table border-separate border-spacing-0 bg-white [&_tbody_tr:hover]:bg-white [&_tbody_tr]:bg-white [&_td]:bg-white [&_th]:bg-white [&_thead_tr]:bg-white">
|
<div class="space-y-4">
|
||||||
<thead>
|
<div class="space-y-1">
|
||||||
<tr>
|
<h2 class="text-lg font-semibold text-[#163624]">Товар в наличии</h2>
|
||||||
<th class="border-b border-base-300">Артикул</th>
|
<p class="text-sm leading-6 text-[#5d7468]">
|
||||||
<th class="border-b border-base-300">Ширина</th>
|
Все доступные варианты по этому типу товара.
|
||||||
<th class="border-b border-base-300">Длина</th>
|
</p>
|
||||||
<th class="border-b border-base-300">Толщина</th>
|
</div>
|
||||||
<th class="border-b border-base-300">Втулка</th>
|
|
||||||
<th class="border-b border-base-300 text-right">Действие</th>
|
<div class="overflow-x-auto rounded-[20px] border border-[#edf4ef] bg-white">
|
||||||
</tr>
|
<table class="table border-separate border-spacing-0 bg-white [&_tbody_tr:hover]:bg-white [&_tbody_tr]:bg-white [&_td]:bg-white [&_th]:bg-white [&_thead_tr]:bg-white">
|
||||||
</thead>
|
<thead>
|
||||||
<tbody>
|
<tr>
|
||||||
<tr v-for="product in selectedGroup.products" :key="`${selectedGroup.key}-${product.id}`">
|
<th class="border-b border-base-300">Артикул</th>
|
||||||
<td class="border-b border-base-200">{{ product.sku }}</td>
|
<th class="border-b border-base-300">Ширина</th>
|
||||||
<td class="border-b border-base-200">{{ product.widthMm ?? '—' }}</td>
|
<th class="border-b border-base-300">Длина</th>
|
||||||
<td class="border-b border-base-200">{{ product.lengthM ?? '—' }}</td>
|
<th class="border-b border-base-300">Толщина</th>
|
||||||
<td class="border-b border-base-200">{{ product.thicknessMicron ?? '—' }}</td>
|
<th class="border-b border-base-300">Втулка</th>
|
||||||
<td class="border-b border-base-200">{{ product.sleeveBrand ?? '—' }}</td>
|
<th class="border-b border-base-300 text-right">Действие</th>
|
||||||
<td class="border-b border-base-200 text-right">
|
</tr>
|
||||||
<button
|
</thead>
|
||||||
v-if="getQuantity(product.id) === 0"
|
<tbody>
|
||||||
class="btn h-9 rounded-full border-0 bg-[#139957] px-4 text-xs font-semibold text-white hover:bg-[#0d854a]"
|
<tr v-for="product in selectedGroup.products" :key="`${selectedGroup.key}-${product.id}`">
|
||||||
@click="incrementProduct(product)"
|
<td class="border-b border-base-200">{{ product.sku }}</td>
|
||||||
>
|
<td class="border-b border-base-200">{{ product.widthMm ?? '—' }}</td>
|
||||||
В корзину
|
<td class="border-b border-base-200">{{ product.lengthM ?? '—' }}</td>
|
||||||
</button>
|
<td class="border-b border-base-200">{{ product.thicknessMicron ?? '—' }}</td>
|
||||||
<div v-else class="ml-auto flex w-28 items-center justify-between rounded-xl border border-base-300 px-1 py-1">
|
<td class="border-b border-base-200">{{ product.sleeveBrand ?? '—' }}</td>
|
||||||
<button
|
<td class="border-b border-base-200 text-right">
|
||||||
class="btn btn-xs btn-square"
|
<button
|
||||||
:disabled="getQuantity(product.id) === 0"
|
v-if="getQuantity(product.id) === 0"
|
||||||
@click="decrementProduct(product.id)"
|
class="btn h-9 rounded-full border-0 bg-[#139957] px-4 text-xs font-semibold text-white hover:bg-[#0d854a]"
|
||||||
>
|
@click="incrementProduct(product)"
|
||||||
-
|
>
|
||||||
</button>
|
В корзину
|
||||||
<span class="min-w-8 text-center text-sm font-semibold">{{ getQuantity(product.id) }}</span>
|
</button>
|
||||||
<button class="btn btn-xs btn-square" @click="incrementProduct(product)">+</button>
|
<div v-else class="ml-auto flex w-28 items-center justify-between rounded-xl border border-base-300 px-1 py-1">
|
||||||
</div>
|
<button
|
||||||
</td>
|
class="btn btn-xs btn-square"
|
||||||
</tr>
|
:disabled="getQuantity(product.id) === 0"
|
||||||
</tbody>
|
@click="decrementProduct(product.id)"
|
||||||
</table>
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<span class="min-w-8 text-center text-sm font-semibold">{{ getQuantity(product.id) }}</span>
|
||||||
|
<button class="btn btn-xs btn-square" @click="incrementProduct(product)">+</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="alert surface-card border-0">Такой тип товара не найден.</div>
|
<div v-else class="alert surface-card border-0">Такой тип товара не найден.</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user