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);
|
||||
}
|
||||
|
||||
function createGroupState(): GroupState {
|
||||
function createGroupState(group: ProductGroup): GroupState {
|
||||
const firstProduct = group.products[0];
|
||||
|
||||
return {
|
||||
widthMm: null,
|
||||
lengthM: null,
|
||||
thicknessMicron: null,
|
||||
sleeveBrand: null,
|
||||
widthMm: firstProduct?.widthMm ?? null,
|
||||
lengthM: firstProduct?.lengthM ?? null,
|
||||
thicknessMicron: firstProduct?.thicknessMicron ?? null,
|
||||
sleeveBrand: firstProduct?.sleeveBrand ?? null,
|
||||
isExpanded: false,
|
||||
};
|
||||
}
|
||||
@@ -216,14 +218,14 @@ watch(
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
groupStates[group.key] ??= createGroupState();
|
||||
groupStates[group.key] ??= createGroupState(group);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function getGroupState(group: ProductGroup) {
|
||||
groupStates[group.key] ??= createGroupState();
|
||||
groupStates[group.key] ??= createGroupState(group);
|
||||
return groupStates[group.key];
|
||||
}
|
||||
|
||||
@@ -252,61 +254,6 @@ function selectedProduct(group: ProductGroup) {
|
||||
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) {
|
||||
const product = selectedProduct(group);
|
||||
if (product?.quantityPerBoxOptions.length) {
|
||||
@@ -349,17 +296,25 @@ function isOptionAvailable(group: ProductGroup, field: ParamFieldKey, option: Pa
|
||||
function updateField(group: ProductGroup, field: ParamFieldKey, value: ParamValue) {
|
||||
const state = getGroupState(group);
|
||||
state[field] = value as GroupState[typeof field];
|
||||
|
||||
const resolved = selectedProduct(group);
|
||||
if (resolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
function clearField(group: ProductGroup, field: ParamFieldKey) {
|
||||
getGroupState(group)[field] = null;
|
||||
const fallback = group.products.find((product) => product[field] === value) ?? group.products[0];
|
||||
if (!fallback) {
|
||||
return;
|
||||
}
|
||||
|
||||
function clearSelection(group: ProductGroup) {
|
||||
const state = getGroupState(group);
|
||||
for (const key of PARAM_KEYS) {
|
||||
state[key] = null;
|
||||
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) {
|
||||
@@ -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="mb-4">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-base-content/45">Тип товара</p>
|
||||
<h2 class="mt-2 text-2xl font-bold text-[#163624]">{{ group.typeLabel }}</h2>
|
||||
<h2 class="text-2xl font-bold text-[#163624]">{{ group.typeLabel }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<fieldset
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="field in visibleFields(group)"
|
||||
: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">
|
||||
<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>
|
||||
<p class="text-sm font-semibold text-[#163624]">{{ field.label }}</p>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<label
|
||||
@@ -487,30 +432,29 @@ function decrementSelected(group: ProductGroup) {
|
||||
<span>{{ formatOptionLabel(field.key, option) }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="space-y-3">
|
||||
<div class="rounded-[22px] bg-base-200/70 p-3">
|
||||
<p v-if="selectedProduct(group)" class="text-sm font-semibold text-[#163624]">{{ selectionHeadline(group) }}</p>
|
||||
<p class="text-xs leading-5 text-base-content/65">{{ selectionDescription(group) }}</p>
|
||||
<p class="mt-2 text-xs leading-5 text-base-content/65">Короб: {{ boxQuantityLabel(group) }}</p>
|
||||
</div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-base-content/45">Артикул</p>
|
||||
<p class="text-xl font-bold text-[#163624]">{{ articleLabel(group) }}</p>
|
||||
<p class="text-xs leading-5 text-base-content/65">Короб: {{ boxQuantityLabel(group) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<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)"
|
||||
@click="incrementSelected(group)"
|
||||
>
|
||||
{{ addButtonLabel(group) }}
|
||||
В корзину
|
||||
</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">
|
||||
<button
|
||||
class="btn btn-square btn-sm"
|
||||
@@ -528,14 +472,6 @@ function decrementSelected(group: ProductGroup) {
|
||||
</button>
|
||||
</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>
|
||||
</aside>
|
||||
@@ -543,24 +479,24 @@ function decrementSelected(group: ProductGroup) {
|
||||
|
||||
<div
|
||||
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>
|
||||
<tr>
|
||||
<th>Фото</th>
|
||||
<th>SKU</th>
|
||||
<th>Ширина</th>
|
||||
<th>Длина</th>
|
||||
<th>Толщина</th>
|
||||
<th>Втулка</th>
|
||||
<th>Короб</th>
|
||||
<th class="text-right">Действие</th>
|
||||
<th class="border-b border-base-300">Фото</th>
|
||||
<th class="border-b border-base-300">Артикул</th>
|
||||
<th class="border-b border-base-300">Ширина</th>
|
||||
<th class="border-b border-base-300">Длина</th>
|
||||
<th class="border-b border-base-300">Толщина</th>
|
||||
<th class="border-b border-base-300">Втулка</th>
|
||||
<th class="border-b border-base-300">Короб</th>
|
||||
<th class="border-b border-base-300 text-right">Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="product in group.products" :key="`${group.key}-${product.id}`">
|
||||
<td>
|
||||
<td class="border-b border-base-200">
|
||||
<img
|
||||
:src="createProductCover(product.name, product.sku)"
|
||||
:alt="`Превью ${product.sku}`"
|
||||
@@ -568,13 +504,13 @@ function decrementSelected(group: ProductGroup) {
|
||||
loading="lazy"
|
||||
>
|
||||
</td>
|
||||
<td>{{ product.sku }}</td>
|
||||
<td>{{ product.widthMm ?? '—' }}</td>
|
||||
<td>{{ product.lengthM ?? '—' }}</td>
|
||||
<td>{{ product.thicknessMicron ?? '—' }}</td>
|
||||
<td>{{ product.sleeveBrand ?? '—' }}</td>
|
||||
<td>{{ product.quantityPerBox ?? '—' }}</td>
|
||||
<td class="text-right">
|
||||
<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>
|
||||
<td class="border-b border-base-200">{{ product.thicknessMicron ?? '—' }}</td>
|
||||
<td class="border-b border-base-200">{{ product.sleeveBrand ?? '—' }}</td>
|
||||
<td class="border-b border-base-200">{{ product.quantityPerBox ?? '—' }}</td>
|
||||
<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">
|
||||
<button
|
||||
class="btn btn-xs btn-square"
|
||||
|
||||
Reference in New Issue
Block a user