Refine catalog detail interactions

This commit is contained in:
Ruslan Bakiev
2026-04-09 19:47:50 +07:00
parent 03ac74e10b
commit e8ff766c24

View File

@@ -484,40 +484,40 @@ function fieldHelperText(group: ProductGroup, field: ParamFieldKey) {
const setting = groupCatalogSetting(group); const setting = groupCatalogSetting(group);
if (field === 'widthMm') { if (field === 'widthMm') {
return 'Нужная ширина рулона.'; return 'Ширина определяет, насколько широкой будет полоса материала в работе и при намотке.';
} }
if (field === 'lengthM') { if (field === 'lengthM') {
const customRange = formatLengthRange(setting); const customRange = formatLengthRange(setting);
if (setting.allowCustomLength && customRange) { if (setting.allowCustomLength && customRange) {
return `Можно выбрать стандартный метраж или свой: ${customRange}.`; return `Можно выбрать стандартный метраж из наличия или заказать свой вариант. Доступный диапазон: ${customRange}.`;
} }
return 'Нужный метраж рулона.'; return 'Длина показывает, сколько метров материала будет в одном рулоне.';
} }
if (field === 'thicknessMicron') { if (field === 'thicknessMicron') {
return 'Плотность и толщина материала.'; return 'Толщина влияет на плотность, прочность и общее ощущение материала в работе.';
} }
if (field === 'sleeveBrand') { if (field === 'sleeveBrand') {
if (setting.allowCustomSleeveBrand) { if (setting.allowCustomSleeveBrand) {
return 'Стандартная втулка или своя с логотипом.'; return 'Можно выбрать стандартную втулку или сделать свою с логотипом под заказ.';
} }
return 'Вариант втулки внутри рулона.'; return 'Втулка находится внутри рулона и влияет на совместимость с вашим оборудованием.';
} }
if (field === 'colorTag') { if (field === 'colorTag') {
return 'Цвет ленты.'; return 'Цвет нужен для визуального отличия, маркировки и внешнего вида готового рулона.';
} }
if (field === 'labelTag') { if (field === 'labelTag') {
if (setting.allowCustomLabel) { if (setting.allowCustomLabel) {
return 'Стандартная маркировка или своя надпись.'; return 'Можно взять стандартную маркировку из каталога или нанести свою надпись.';
} }
return 'Готовая надпись или маркировка.'; return 'Надпись или маркировка помогает сразу выбрать нужный готовый вариант.';
} }
return 'Параметр товара.'; return 'Параметр товара.';
@@ -543,29 +543,41 @@ function customizationDetails(group: ProductGroup) {
return details; return details;
} }
function productBadges(product: ParsedProduct) { function totalAvailableQty(product: ParsedProduct) {
const badges: string[] = []; return product.availableInWarehouses.reduce((sum, balance) => sum + Number(balance.availableQty || 0), 0);
}
if (product.widthMm !== null) { function warehouseAvailability(product: ParsedProduct) {
badges.push(`${product.widthMm} мм`); return product.availableInWarehouses
.filter((balance) => Number(balance.availableQty || 0) > 0)
.map((balance) => `${balance.warehouse.code}: ${balance.availableQty}`)
.join(' · ');
}
function availabilityTone(product: ParsedProduct) {
const qty = totalAvailableQty(product);
if (qty <= 0) {
return 'bg-[#d95c5c]';
} }
if (product.lengthM !== null) { if (qty < 20) {
badges.push(`${product.lengthM} м`); return 'bg-[#e2b534]';
}
if (product.thicknessMicron !== null) {
badges.push(`${product.thicknessMicron} мкм`);
}
if (product.sleeveBrand) {
badges.push(product.sleeveBrand);
}
if (product.colorTags[0]) {
badges.push(product.colorTags[0]);
}
if (product.labelTags[0]) {
badges.push(product.labelTags[0]);
} }
return badges; return 'bg-[#2aa36b]';
}
function availabilityLabel(product: ParsedProduct) {
const qty = totalAvailableQty(product);
if (qty <= 0) {
return 'Нет в наличии';
}
if (qty < 20) {
return 'Остаток ограничен';
}
return 'В наличии';
} }
function incrementProduct(product: ProductNode) { function incrementProduct(product: ProductNode) {
@@ -618,7 +630,7 @@ function productDetailPath(group: ProductGroup) {
<NuxtLink <NuxtLink
v-if="previousGroup" v-if="previousGroup"
:to="productDetailPath(previousGroup)" :to="productDetailPath(previousGroup)"
class="absolute left-0 top-28 z-10 hidden w-44 -translate-x-[92%] rounded-[28px] border border-[#e6efe9] bg-white p-3 shadow-[0_20px_40px_rgba(18,56,36,0.08)] transition hover:-translate-x-[98%] hover:shadow-[0_28px_48px_rgba(18,56,36,0.12)] 2xl:block" class="absolute left-0 top-28 z-10 hidden w-44 -translate-x-[108%] rounded-[28px] border border-[#e6efe9] bg-white p-3 shadow-[0_20px_40px_rgba(18,56,36,0.08)] transition hover:-translate-x-[114%] hover:shadow-[0_28px_48px_rgba(18,56,36,0.12)] 2xl:block"
> >
<img <img
:src="createProductCover(previousGroup.typeLabel, previousGroup.key)" :src="createProductCover(previousGroup.typeLabel, previousGroup.key)"
@@ -632,7 +644,7 @@ function productDetailPath(group: ProductGroup) {
<NuxtLink <NuxtLink
v-if="nextGroup" v-if="nextGroup"
:to="productDetailPath(nextGroup)" :to="productDetailPath(nextGroup)"
class="absolute right-0 top-28 z-10 hidden w-44 translate-x-[92%] rounded-[28px] border border-[#e6efe9] bg-white p-3 shadow-[0_20px_40px_rgba(18,56,36,0.08)] transition hover:translate-x-[98%] hover:shadow-[0_28px_48px_rgba(18,56,36,0.12)] 2xl:block" class="absolute right-0 top-28 z-10 hidden w-44 translate-x-[108%] rounded-[28px] border border-[#e6efe9] bg-white p-3 shadow-[0_20px_40px_rgba(18,56,36,0.08)] transition hover:translate-x-[114%] hover:shadow-[0_28px_48px_rgba(18,56,36,0.12)] 2xl:block"
> >
<img <img
:src="createProductCover(nextGroup.typeLabel, nextGroup.key)" :src="createProductCover(nextGroup.typeLabel, nextGroup.key)"
@@ -727,7 +739,7 @@ function productDetailPath(group: ProductGroup) {
class="cursor-pointer rounded-2xl border px-4 py-2 text-sm font-medium transition" class="cursor-pointer rounded-2xl border px-4 py-2 text-sm font-medium transition"
:class="[ :class="[
getGroupState(selectedGroup)[field.key] === option getGroupState(selectedGroup)[field.key] === option
? 'border-[#163624] bg-[#163624] text-white shadow-[0_12px_24px_rgba(22,54,36,0.18)]' ? 'border-[#163624] bg-[#163624] text-white'
: isOptionAvailable(selectedGroup, field.key, option) : isOptionAvailable(selectedGroup, field.key, option)
? 'border-[#dce9e1] bg-white text-[#163624] hover:border-[#163624]' ? 'border-[#dce9e1] bg-white text-[#163624] hover:border-[#163624]'
: 'border-[#e6eaee] bg-[#f3f5f7] text-[#8a949d]', : 'border-[#e6eaee] bg-[#f3f5f7] text-[#8a949d]',
@@ -744,7 +756,10 @@ function productDetailPath(group: ProductGroup) {
</label> </label>
</div> </div>
<p class="mt-2 text-sm leading-6 text-[#607569]">{{ fieldHelperText(selectedGroup, field.key) }}</p> <details class="mt-3 rounded-[20px] bg-[#f7fbf8] px-4 py-3 text-sm text-[#587064]">
<summary class="cursor-pointer font-medium text-[#355947]">Подробнее</summary>
<p class="mt-2 leading-6">{{ fieldHelperText(selectedGroup, field.key) }}</p>
</details>
</article> </article>
</div> </div>
@@ -789,52 +804,69 @@ function productDetailPath(group: ProductGroup) {
<div class="rounded-[32px] border border-[#e6efe9] bg-white p-5 shadow-[0_20px_40px_rgba(18,56,36,0.06)]"> <div class="rounded-[32px] border border-[#e6efe9] bg-white p-5 shadow-[0_20px_40px_rgba(18,56,36,0.06)]">
<p class="text-base font-semibold text-[#163624]">Доступные варианты</p> <p class="text-base font-semibold text-[#163624]">Доступные варианты</p>
<div class="mt-4 space-y-3"> <div class="mt-4 overflow-x-auto rounded-[24px] border border-[#edf4ef]">
<article <table class="table bg-white">
v-for="product in selectedGroup.products" <thead>
:key="`${selectedGroup.key}-${product.id}`" <tr class="text-[#587064]">
class="flex flex-col gap-4 rounded-[24px] border border-[#edf4ef] bg-[#fbfcfb] p-4 lg:flex-row lg:items-center lg:justify-between" <th>SKU</th>
> <th>Ширина</th>
<div class="min-w-0"> <th>Длина</th>
<p class="text-base font-semibold text-[#163624]">{{ product.sku }}</p> <th>Толщина</th>
<th>Втулка</th>
<div class="mt-3 flex flex-wrap gap-2"> <th>Цвет</th>
<span <th>Надпись</th>
v-for="badge in productBadges(product)" <th>Остаток</th>
:key="`${product.id}-${badge}`" <th class="text-right">Действие</th>
class="rounded-full border border-[#dce9e1] bg-white px-3 py-1.5 text-xs font-medium text-[#355947]" </tr>
> </thead>
{{ badge }} <tbody>
</span> <tr v-for="product in selectedGroup.products" :key="`${selectedGroup.key}-${product.id}`" class="align-middle">
</div> <td class="font-semibold text-[#163624]">{{ product.sku }}</td>
</div> <td>{{ product.widthMm ?? '—' }}</td>
<td>{{ product.lengthM ?? '—' }}</td>
<div class="flex shrink-0 items-center justify-end"> <td>{{ product.thicknessMicron ?? '—' }}</td>
<button <td>{{ product.sleeveBrand ?? '—' }}</td>
v-if="getQuantity(product.id) === 0" <td>{{ product.colorTags.join(', ') || '—' }}</td>
class="btn h-10 rounded-full border-0 bg-[#139957] px-5 text-sm font-semibold text-white hover:bg-[#0d854a]" <td>{{ product.labelTags.join(', ') || '—' }}</td>
@click="incrementProduct(product)" <td>
> <div class="flex min-w-[180px] items-center gap-3">
В корзину <span class="h-3 w-3 rounded-sm" :class="availabilityTone(product)" />
</button> <div class="min-w-0">
<div v-else class="ml-auto flex w-32 items-center justify-between rounded-[20px] border border-[#dce9e1] bg-white px-2 py-2"> <p class="text-sm font-medium text-[#163624]">{{ availabilityLabel(product) }}</p>
<button <p class="text-xs text-[#607569]">
class="btn btn-xs btn-square border-0 bg-[#f3f7f4] text-[#163624] shadow-none hover:bg-[#eaf2ec]" {{ totalAvailableQty(product) }}<span v-if="warehouseAvailability(product)"> · {{ warehouseAvailability(product) }}</span>
:disabled="getQuantity(product.id) === 0" </p>
@click="decrementProduct(product.id)" </div>
> </div>
- </td>
</button> <td class="text-right">
<span class="min-w-8 text-center text-sm font-semibold text-[#163624]">{{ getQuantity(product.id) }}</span> <button
<button v-if="getQuantity(product.id) === 0"
class="btn btn-xs btn-square border-0 bg-[#f3f7f4] text-[#163624] shadow-none hover:bg-[#eaf2ec]" class="btn h-10 rounded-full border-0 bg-[#139957] px-5 text-sm font-semibold text-white hover:bg-[#0d854a]"
@click="incrementProduct(product)" @click="incrementProduct(product)"
> >
+ В корзину
</button> </button>
</div> <div v-else class="ml-auto flex w-32 items-center justify-between rounded-[20px] border border-[#dce9e1] bg-white px-2 py-2">
</div> <button
</article> class="btn btn-xs btn-square border-0 bg-[#f3f7f4] text-[#163624] shadow-none hover:bg-[#eaf2ec]"
:disabled="getQuantity(product.id) === 0"
@click="decrementProduct(product.id)"
>
-
</button>
<span class="min-w-8 text-center text-sm font-semibold text-[#163624]">{{ getQuantity(product.id) }}</span>
<button
class="btn btn-xs btn-square border-0 bg-[#f3f7f4] text-[#163624] shadow-none hover:bg-[#eaf2ec]"
@click="incrementProduct(product)"
>
+
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>