refactor(catalog): default valid selections and minimal action panel
This commit is contained in:
@@ -194,12 +194,14 @@ function requiredKeys(group: ProductGroup) {
|
|||||||
return visibleFields(group).map((field) => field.key);
|
return visibleFields(group).map((field) => field.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createGroupState(): GroupState {
|
function createGroupState(group: ProductGroup): GroupState {
|
||||||
|
const firstProduct = group.products[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
widthMm: null,
|
widthMm: firstProduct?.widthMm ?? null,
|
||||||
lengthM: null,
|
lengthM: firstProduct?.lengthM ?? null,
|
||||||
thicknessMicron: null,
|
thicknessMicron: firstProduct?.thicknessMicron ?? null,
|
||||||
sleeveBrand: null,
|
sleeveBrand: firstProduct?.sleeveBrand ?? null,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -216,14 +218,14 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
groupStates[group.key] ??= createGroupState();
|
groupStates[group.key] ??= createGroupState(group);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
function getGroupState(group: ProductGroup) {
|
function getGroupState(group: ProductGroup) {
|
||||||
groupStates[group.key] ??= createGroupState();
|
groupStates[group.key] ??= createGroupState(group);
|
||||||
return groupStates[group.key];
|
return groupStates[group.key];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,61 +254,6 @@ function selectedProduct(group: ProductGroup) {
|
|||||||
return matches.length === 1 ? matches[0] : null;
|
return matches.length === 1 ? matches[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function remainingSelectionCount(group: ProductGroup) {
|
|
||||||
const state = getGroupState(group);
|
|
||||||
return requiredKeys(group).filter((key) => state[key] === null).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pluralize(value: number, one: string, few: string, many: string) {
|
|
||||||
const mod10 = value % 10;
|
|
||||||
const mod100 = value % 100;
|
|
||||||
|
|
||||||
if (mod10 === 1 && mod100 !== 11) {
|
|
||||||
return one;
|
|
||||||
}
|
|
||||||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) {
|
|
||||||
return few;
|
|
||||||
}
|
|
||||||
return many;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectionHeadline(group: ProductGroup) {
|
|
||||||
const product = selectedProduct(group);
|
|
||||||
if (product) {
|
|
||||||
return `SKU ${product.sku}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const remaining = remainingSelectionCount(group);
|
|
||||||
if (remaining > 0) {
|
|
||||||
return `Выберите еще ${remaining} ${pluralize(remaining, 'параметр', 'параметра', 'параметров')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Комбинация не найдена';
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectionDescription(group: ProductGroup) {
|
|
||||||
const product = selectedProduct(group);
|
|
||||||
if (product) {
|
|
||||||
return [
|
|
||||||
product.widthMm ? `${product.widthMm} мм` : null,
|
|
||||||
product.lengthM ? `${product.lengthM} м` : null,
|
|
||||||
product.thicknessMicron ? `${product.thicknessMicron} мкм` : null,
|
|
||||||
normalizeText(product.sleeveBrand) || null,
|
|
||||||
].filter(Boolean).join(' • ');
|
|
||||||
}
|
|
||||||
|
|
||||||
const remaining = remainingSelectionCount(group);
|
|
||||||
if (remaining > 0) {
|
|
||||||
return 'Переключатели ниже собирают точную модификацию товара.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Разверните весь список, если нужна ручная проверка вариантов.';
|
|
||||||
}
|
|
||||||
|
|
||||||
function addButtonLabel(group: ProductGroup) {
|
|
||||||
return selectedProduct(group) ? 'В корзину' : 'Выбрать параметры';
|
|
||||||
}
|
|
||||||
|
|
||||||
function boxQuantityLabel(group: ProductGroup) {
|
function boxQuantityLabel(group: ProductGroup) {
|
||||||
const product = selectedProduct(group);
|
const product = selectedProduct(group);
|
||||||
if (product?.quantityPerBoxOptions.length) {
|
if (product?.quantityPerBoxOptions.length) {
|
||||||
@@ -349,17 +296,25 @@ function isOptionAvailable(group: ProductGroup, field: ParamFieldKey, option: Pa
|
|||||||
function updateField(group: ProductGroup, field: ParamFieldKey, value: ParamValue) {
|
function updateField(group: ProductGroup, field: ParamFieldKey, value: ParamValue) {
|
||||||
const state = getGroupState(group);
|
const state = getGroupState(group);
|
||||||
state[field] = value as GroupState[typeof field];
|
state[field] = value as GroupState[typeof field];
|
||||||
}
|
|
||||||
|
|
||||||
function clearField(group: ProductGroup, field: ParamFieldKey) {
|
const resolved = selectedProduct(group);
|
||||||
getGroupState(group)[field] = null;
|
if (resolved) {
|
||||||
}
|
return;
|
||||||
|
|
||||||
function clearSelection(group: ProductGroup) {
|
|
||||||
const state = getGroupState(group);
|
|
||||||
for (const key of PARAM_KEYS) {
|
|
||||||
state[key] = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fallback = group.products.find((product) => product[field] === value) ?? group.products[0];
|
||||||
|
if (!fallback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.widthMm = fallback.widthMm ?? null;
|
||||||
|
state.lengthM = fallback.lengthM ?? null;
|
||||||
|
state.thicknessMicron = fallback.thicknessMicron ?? null;
|
||||||
|
state.sleeveBrand = fallback.sleeveBrand ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function articleLabel(group: ProductGroup) {
|
||||||
|
return selectedProduct(group)?.sku ?? '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleExpanded(group: ProductGroup) {
|
function toggleExpanded(group: ProductGroup) {
|
||||||
@@ -441,26 +396,16 @@ function decrementSelected(group: ProductGroup) {
|
|||||||
|
|
||||||
<div class="rounded-[28px] bg-base-100 p-4 md:p-5 xl:col-span-4">
|
<div class="rounded-[28px] bg-base-100 p-4 md:p-5 xl:col-span-4">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-base-content/45">Тип товара</p>
|
<h2 class="text-2xl font-bold text-[#163624]">{{ group.typeLabel }}</h2>
|
||||||
<h2 class="mt-2 text-2xl font-bold text-[#163624]">{{ group.typeLabel }}</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="space-y-4">
|
||||||
<fieldset
|
<div
|
||||||
v-for="field in visibleFields(group)"
|
v-for="field in visibleFields(group)"
|
||||||
:key="`${group.key}-${field.key}`"
|
:key="`${group.key}-${field.key}`"
|
||||||
class="rounded-[24px] bg-base-200/50 p-4"
|
class="border-b border-base-200 pb-4 last:border-b-0 last:pb-0"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between gap-3">
|
<p class="text-sm font-semibold text-[#163624]">{{ field.label }}</p>
|
||||||
<legend class="text-sm font-semibold text-[#163624]">{{ field.label }}</legend>
|
|
||||||
<button
|
|
||||||
v-if="getGroupState(group)[field.key] !== null"
|
|
||||||
class="btn btn-ghost btn-xs rounded-full px-2"
|
|
||||||
@click="clearField(group, field.key)"
|
|
||||||
>
|
|
||||||
Сбросить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3 flex flex-wrap gap-2">
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
<label
|
<label
|
||||||
@@ -487,30 +432,29 @@ function decrementSelected(group: ProductGroup) {
|
|||||||
<span>{{ formatOptionLabel(field.key, option) }}</span>
|
<span>{{ formatOptionLabel(field.key, option) }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="rounded-[28px] bg-base-100 p-4 md:p-5 xl:col-span-1">
|
<aside class="rounded-[28px] bg-base-100 p-4 md:p-5 xl:col-span-1">
|
||||||
<div class="flex h-full flex-col justify-between gap-4">
|
<div class="flex h-full flex-col justify-between gap-4">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="rounded-[22px] bg-base-200/70 p-3">
|
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-base-content/45">Артикул</p>
|
||||||
<p v-if="selectedProduct(group)" class="text-sm font-semibold text-[#163624]">{{ selectionHeadline(group) }}</p>
|
<p class="text-xl font-bold text-[#163624]">{{ articleLabel(group) }}</p>
|
||||||
<p class="text-xs leading-5 text-base-content/65">{{ selectionDescription(group) }}</p>
|
<p class="text-xs leading-5 text-base-content/65">Короб: {{ boxQuantityLabel(group) }}</p>
|
||||||
<p class="mt-2 text-xs leading-5 text-base-content/65">Короб: {{ boxQuantityLabel(group) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<button
|
<button
|
||||||
class="btn btn-neutral h-11 w-full rounded-full text-sm font-semibold"
|
v-if="selectedQty(group) === 0"
|
||||||
|
class="btn btn-success h-11 w-full rounded-full border-0 text-sm font-semibold text-success-content"
|
||||||
:disabled="!selectedProduct(group)"
|
:disabled="!selectedProduct(group)"
|
||||||
@click="incrementSelected(group)"
|
@click="incrementSelected(group)"
|
||||||
>
|
>
|
||||||
{{ addButtonLabel(group) }}
|
В корзину
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="rounded-[22px] border border-base-300 bg-base-100 px-2 py-1">
|
<div v-else class="rounded-[22px] border border-base-300 bg-base-100 px-2 py-1">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-square btn-sm"
|
class="btn btn-square btn-sm"
|
||||||
@@ -528,14 +472,6 @@ function decrementSelected(group: ProductGroup) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost btn-sm w-full rounded-full"
|
|
||||||
:disabled="requiredKeys(group).every((key) => getGroupState(group)[key] === null)"
|
|
||||||
@click="clearSelection(group)"
|
|
||||||
>
|
|
||||||
Сбросить выбор
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -543,24 +479,24 @@ function decrementSelected(group: ProductGroup) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="getGroupState(group).isExpanded"
|
v-if="getGroupState(group).isExpanded"
|
||||||
class="mt-4 overflow-x-auto rounded-[28px] bg-base-100 p-2"
|
class="mt-4 overflow-x-auto rounded-[28px] bg-base-100"
|
||||||
>
|
>
|
||||||
<table class="table table-zebra">
|
<table class="table border-separate border-spacing-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Фото</th>
|
<th class="border-b border-base-300">Фото</th>
|
||||||
<th>SKU</th>
|
<th class="border-b border-base-300">Артикул</th>
|
||||||
<th>Ширина</th>
|
<th class="border-b border-base-300">Ширина</th>
|
||||||
<th>Длина</th>
|
<th class="border-b border-base-300">Длина</th>
|
||||||
<th>Толщина</th>
|
<th class="border-b border-base-300">Толщина</th>
|
||||||
<th>Втулка</th>
|
<th class="border-b border-base-300">Втулка</th>
|
||||||
<th>Короб</th>
|
<th class="border-b border-base-300">Короб</th>
|
||||||
<th class="text-right">Действие</th>
|
<th class="border-b border-base-300 text-right">Действие</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="product in group.products" :key="`${group.key}-${product.id}`">
|
<tr v-for="product in group.products" :key="`${group.key}-${product.id}`">
|
||||||
<td>
|
<td class="border-b border-base-200">
|
||||||
<img
|
<img
|
||||||
:src="createProductCover(product.name, product.sku)"
|
:src="createProductCover(product.name, product.sku)"
|
||||||
:alt="`Превью ${product.sku}`"
|
:alt="`Превью ${product.sku}`"
|
||||||
@@ -568,13 +504,13 @@ function decrementSelected(group: ProductGroup) {
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ product.sku }}</td>
|
<td class="border-b border-base-200">{{ product.sku }}</td>
|
||||||
<td>{{ product.widthMm ?? '—' }}</td>
|
<td class="border-b border-base-200">{{ product.widthMm ?? '—' }}</td>
|
||||||
<td>{{ product.lengthM ?? '—' }}</td>
|
<td class="border-b border-base-200">{{ product.lengthM ?? '—' }}</td>
|
||||||
<td>{{ product.thicknessMicron ?? '—' }}</td>
|
<td class="border-b border-base-200">{{ product.thicknessMicron ?? '—' }}</td>
|
||||||
<td>{{ product.sleeveBrand ?? '—' }}</td>
|
<td class="border-b border-base-200">{{ product.sleeveBrand ?? '—' }}</td>
|
||||||
<td>{{ product.quantityPerBox ?? '—' }}</td>
|
<td class="border-b border-base-200">{{ product.quantityPerBox ?? '—' }}</td>
|
||||||
<td class="text-right">
|
<td class="border-b border-base-200 text-right">
|
||||||
<div class="ml-auto flex w-28 items-center justify-between rounded-xl border border-base-300 px-1 py-1">
|
<div class="ml-auto flex w-28 items-center justify-between rounded-xl border border-base-300 px-1 py-1">
|
||||||
<button
|
<button
|
||||||
class="btn btn-xs btn-square"
|
class="btn btn-xs btn-square"
|
||||||
|
|||||||
Reference in New Issue
Block a user