Group catalog variants by product type
This commit is contained in:
@@ -4,19 +4,20 @@ import { ClientProductsDocument, type ClientProductsQuery } from '~/composables/
|
|||||||
import { useClientCart } from '~/composables/useClientCart';
|
import { useClientCart } from '~/composables/useClientCart';
|
||||||
|
|
||||||
type ProductNode = ClientProductsQuery['clientProducts'][number];
|
type ProductNode = ClientProductsQuery['clientProducts'][number];
|
||||||
type ParamFieldKey = 'widthMm' | 'lengthM' | 'thicknessMicron' | 'sleeveBrand' | 'quantityPerBox';
|
type ParamFieldKey = 'widthMm' | 'lengthM' | 'thicknessMicron' | 'sleeveBrand' | 'quantityPerBox' | 'colorTag' | 'labelTag';
|
||||||
type ParamValue = number | string;
|
type ParamValue = number | string;
|
||||||
|
|
||||||
type ParsedProduct = ProductNode & {
|
type ParsedProduct = ProductNode & {
|
||||||
productTypeLabel: string;
|
productTypeLabel: string;
|
||||||
quantityPerBoxOptions: string[];
|
quantityPerBoxOptions: string[];
|
||||||
normalizedTags: string[];
|
normalizedTags: string[];
|
||||||
|
colorTags: string[];
|
||||||
|
labelTags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProductGroup = {
|
type ProductGroup = {
|
||||||
key: string;
|
key: string;
|
||||||
typeLabel: string;
|
typeLabel: string;
|
||||||
tags: string[];
|
|
||||||
products: ParsedProduct[];
|
products: ParsedProduct[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,16 +27,22 @@ type GroupState = {
|
|||||||
thicknessMicron: number | null;
|
thicknessMicron: number | null;
|
||||||
sleeveBrand: string | null;
|
sleeveBrand: string | null;
|
||||||
quantityPerBox: string | null;
|
quantityPerBox: string | null;
|
||||||
|
colorTag: string | null;
|
||||||
|
labelTag: string | null;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PARAM_KEYS: ParamFieldKey[] = ['widthMm', 'lengthM', 'thicknessMicron', 'sleeveBrand', 'quantityPerBox'];
|
const COLOR_TAGS = ['прозрачный', 'коричневый', 'белый', 'черный', 'желтый', 'зеленый', 'красный', 'синий', 'оранжевый', 'красно-белый'];
|
||||||
|
const LABEL_TAGS = ['хрупкое', 'подарок', 'акция'];
|
||||||
|
const PARAM_KEYS: ParamFieldKey[] = ['widthMm', 'lengthM', 'thicknessMicron', 'sleeveBrand', 'quantityPerBox', 'colorTag', 'labelTag'];
|
||||||
const parameterFields: Array<{ key: ParamFieldKey; label: string }> = [
|
const parameterFields: Array<{ key: ParamFieldKey; label: string }> = [
|
||||||
{ key: 'widthMm', label: 'Ширина' },
|
{ key: 'widthMm', label: 'Ширина' },
|
||||||
{ key: 'lengthM', label: 'Длина' },
|
{ key: 'lengthM', label: 'Длина' },
|
||||||
{ key: 'thicknessMicron', label: 'Толщина' },
|
{ key: 'thicknessMicron', label: 'Толщина' },
|
||||||
{ key: 'sleeveBrand', label: 'Втулка' },
|
{ key: 'sleeveBrand', label: 'Втулка' },
|
||||||
{ key: 'quantityPerBox', label: 'Короб' },
|
{ key: 'quantityPerBox', label: 'Короб' },
|
||||||
|
{ key: 'colorTag', label: 'Цвет' },
|
||||||
|
{ key: 'labelTag', label: 'Надпись' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const coverPresets = [
|
const coverPresets = [
|
||||||
@@ -87,11 +94,15 @@ function createProductCover(name: string, sku: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hydrateProduct(product: ProductNode): ParsedProduct {
|
function hydrateProduct(product: ProductNode): ParsedProduct {
|
||||||
|
const normalizedTags = product.tags.map((tag) => normalizeText(tag)).filter(Boolean).sort((a, b) => a.localeCompare(b, 'ru'));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
productTypeLabel: normalizeText(product.productType) || 'Без типа',
|
productTypeLabel: normalizeText(product.productType) || 'Без типа',
|
||||||
quantityPerBoxOptions: splitBoxValues(product.quantityPerBox),
|
quantityPerBoxOptions: splitBoxValues(product.quantityPerBox),
|
||||||
normalizedTags: product.tags.map((tag) => normalizeText(tag)).filter(Boolean).sort((a, b) => a.localeCompare(b, 'ru')),
|
normalizedTags,
|
||||||
|
colorTags: normalizedTags.filter((tag) => COLOR_TAGS.includes(tag)),
|
||||||
|
labelTags: normalizedTags.filter((tag) => LABEL_TAGS.includes(tag)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,43 +164,25 @@ const productGroups = computed<ProductGroup[]>(() => {
|
|||||||
const map = new Map<string, ParsedProduct[]>();
|
const map = new Map<string, ParsedProduct[]>();
|
||||||
|
|
||||||
for (const product of parsedProducts.value) {
|
for (const product of parsedProducts.value) {
|
||||||
const tagsKey = product.normalizedTags.join('|');
|
const existing = map.get(product.productTypeLabel);
|
||||||
const groupKey = `${product.productTypeLabel}::${tagsKey}`;
|
|
||||||
const existing = map.get(groupKey);
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.push(product);
|
existing.push(product);
|
||||||
} else {
|
} else {
|
||||||
map.set(groupKey, [product]);
|
map.set(product.productTypeLabel, [product]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...map.entries()]
|
return [...map.entries()]
|
||||||
.sort((a, b) => {
|
.sort((a, b) => a[0].localeCompare(b[0], 'ru'))
|
||||||
const firstProduct = a[1][0];
|
.map(([typeLabel, products]) => ({
|
||||||
const secondProduct = b[1][0];
|
key: typeLabel
|
||||||
const byType = firstProduct.productTypeLabel.localeCompare(secondProduct.productTypeLabel, 'ru');
|
|
||||||
if (byType !== 0) {
|
|
||||||
return byType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstProduct.normalizedTags.join('|').localeCompare(secondProduct.normalizedTags.join('|'), 'ru');
|
|
||||||
})
|
|
||||||
.map(([groupSignature, products]) => {
|
|
||||||
const firstProduct = products[0];
|
|
||||||
const key = groupSignature
|
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replaceAll(/[^a-z0-9а-яё|]+/gi, '-')
|
.replaceAll(/[^a-z0-9а-яё]+/gi, '-')
|
||||||
.replaceAll('|', '--')
|
|
||||||
.replaceAll(/-+/g, '-')
|
.replaceAll(/-+/g, '-')
|
||||||
.replaceAll(/^-|-$/g, '');
|
.replaceAll(/^-|-$/g, ''),
|
||||||
|
typeLabel,
|
||||||
return {
|
|
||||||
key,
|
|
||||||
typeLabel: firstProduct.productTypeLabel,
|
|
||||||
tags: firstProduct.normalizedTags,
|
|
||||||
products: [...products].sort(compareProducts),
|
products: [...products].sort(compareProducts),
|
||||||
};
|
}));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function sortParamValues(values: ParamValue[]) {
|
function sortParamValues(values: ParamValue[]) {
|
||||||
@@ -212,6 +205,20 @@ function getAllFieldOptions(group: ProductGroup, field: ParamFieldKey) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field === 'colorTag') {
|
||||||
|
for (const tag of product.colorTags) {
|
||||||
|
values.add(tag);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'labelTag') {
|
||||||
|
for (const tag of product.labelTags) {
|
||||||
|
values.add(tag);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const value = product[field];
|
const value = product[field];
|
||||||
if (value !== null && value !== undefined) {
|
if (value !== null && value !== undefined) {
|
||||||
values.add(value);
|
values.add(value);
|
||||||
@@ -235,7 +242,7 @@ function visibleFieldsByColumn(group: ProductGroup) {
|
|||||||
|
|
||||||
const rightColumn = parameterFields.filter((field) => (
|
const rightColumn = parameterFields.filter((field) => (
|
||||||
visibleKeys.has(field.key)
|
visibleKeys.has(field.key)
|
||||||
&& ['thicknessMicron', 'quantityPerBox', 'sleeveBrand'].includes(field.key)
|
&& ['thicknessMicron', 'quantityPerBox', 'sleeveBrand', 'colorTag', 'labelTag'].includes(field.key)
|
||||||
));
|
));
|
||||||
|
|
||||||
return { leftColumn, rightColumn };
|
return { leftColumn, rightColumn };
|
||||||
@@ -254,6 +261,8 @@ function createGroupState(group: ProductGroup): GroupState {
|
|||||||
thicknessMicron: firstProduct?.thicknessMicron ?? null,
|
thicknessMicron: firstProduct?.thicknessMicron ?? null,
|
||||||
sleeveBrand: firstProduct?.sleeveBrand ?? null,
|
sleeveBrand: firstProduct?.sleeveBrand ?? null,
|
||||||
quantityPerBox: firstProduct?.quantityPerBoxOptions[0] ?? null,
|
quantityPerBox: firstProduct?.quantityPerBoxOptions[0] ?? null,
|
||||||
|
colorTag: firstProduct?.colorTags[0] ?? null,
|
||||||
|
labelTag: firstProduct?.labelTags[0] ?? null,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -291,6 +300,14 @@ function matchesProductState(product: ParsedProduct, state: GroupState, keys: Pa
|
|||||||
return product.quantityPerBoxOptions.includes(String(state[key]));
|
return product.quantityPerBoxOptions.includes(String(state[key]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key === 'colorTag') {
|
||||||
|
return product.colorTags.includes(String(state[key]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'labelTag') {
|
||||||
|
return product.labelTags.includes(String(state[key]));
|
||||||
|
}
|
||||||
|
|
||||||
return product[key] === state[key];
|
return product[key] === state[key];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -329,6 +346,14 @@ function productHasOption(product: ParsedProduct, field: ParamFieldKey, option:
|
|||||||
return product.quantityPerBoxOptions.includes(String(option));
|
return product.quantityPerBoxOptions.includes(String(option));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field === 'colorTag') {
|
||||||
|
return product.colorTags.includes(String(option));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'labelTag') {
|
||||||
|
return product.labelTags.includes(String(option));
|
||||||
|
}
|
||||||
|
|
||||||
return product[field] === option;
|
return product[field] === option;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,6 +410,8 @@ function applyProductToState(state: GroupState, product: ParsedProduct, preferre
|
|||||||
state.lengthM = product.lengthM ?? null;
|
state.lengthM = product.lengthM ?? null;
|
||||||
state.thicknessMicron = product.thicknessMicron ?? null;
|
state.thicknessMicron = product.thicknessMicron ?? null;
|
||||||
state.sleeveBrand = product.sleeveBrand ?? null;
|
state.sleeveBrand = product.sleeveBrand ?? null;
|
||||||
|
state.colorTag = product.colorTags[0] ?? null;
|
||||||
|
state.labelTag = product.labelTags[0] ?? null;
|
||||||
|
|
||||||
if (preferredBoxOption !== null && product.quantityPerBoxOptions.includes(String(preferredBoxOption))) {
|
if (preferredBoxOption !== null && product.quantityPerBoxOptions.includes(String(preferredBoxOption))) {
|
||||||
state.quantityPerBox = String(preferredBoxOption);
|
state.quantityPerBox = String(preferredBoxOption);
|
||||||
@@ -487,16 +514,6 @@ function decrementSelected(group: ProductGroup) {
|
|||||||
<div class="p-4 md:p-5 xl:col-span-4">
|
<div class="p-4 md:p-5 xl:col-span-4">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h2 class="text-2xl font-bold text-[#163624]">{{ group.typeLabel }}</h2>
|
<h2 class="text-2xl font-bold text-[#163624]">{{ group.typeLabel }}</h2>
|
||||||
|
|
||||||
<div v-if="group.tags.length" class="mt-3 flex flex-wrap gap-2">
|
|
||||||
<span
|
|
||||||
v-for="tag in group.tags"
|
|
||||||
:key="`${group.key}-${tag}`"
|
|
||||||
class="rounded-full bg-[#eef8f1] px-3 py-1 text-xs font-semibold uppercase tracking-[0.06em] text-[#15613d]"
|
|
||||||
>
|
|
||||||
{{ tag }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-8 md:grid-cols-2">
|
<div class="grid gap-8 md:grid-cols-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user