refactor(catalog): use structured product toggles
This commit is contained in:
@@ -9,11 +9,6 @@ type ParamValue = number | string;
|
||||
|
||||
type ParsedProduct = ProductNode & {
|
||||
productTypeLabel: string;
|
||||
widthMm: number | null;
|
||||
lengthM: number | null;
|
||||
thicknessMicron: number | null;
|
||||
sleeveBrand: string | null;
|
||||
quantityPerBox: string | null;
|
||||
quantityPerBoxOptions: string[];
|
||||
};
|
||||
|
||||
@@ -33,10 +28,10 @@ type GroupState = {
|
||||
|
||||
const PARAM_KEYS: ParamFieldKey[] = ['widthMm', 'lengthM', 'thicknessMicron', 'sleeveBrand'];
|
||||
const parameterFields: Array<{ key: ParamFieldKey; label: string }> = [
|
||||
{ key: 'widthMm', label: 'Ширина, мм' },
|
||||
{ key: 'lengthM', label: 'Длина, м' },
|
||||
{ key: 'thicknessMicron', label: 'Толщина, мкм' },
|
||||
{ key: 'sleeveBrand', label: 'Бренд втулки' },
|
||||
{ key: 'widthMm', label: 'Ширина' },
|
||||
{ key: 'lengthM', label: 'Длина' },
|
||||
{ key: 'thicknessMicron', label: 'Толщина' },
|
||||
{ key: 'sleeveBrand', label: 'Втулка' },
|
||||
];
|
||||
|
||||
const coverPresets = [
|
||||
@@ -50,8 +45,15 @@ const search = ref('');
|
||||
const groupStates = reactive<Record<string, GroupState>>({});
|
||||
const { addProduct, getQuantity, incrementQuantity, decrementQuantity } = useClientCart();
|
||||
|
||||
function normalizeText(value: string) {
|
||||
return value.replaceAll(/\s+/g, ' ').trim();
|
||||
function normalizeText(value: string | null | undefined) {
|
||||
return String(value ?? '').replaceAll(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function splitBoxValues(value: string | null | undefined) {
|
||||
return normalizeText(value)
|
||||
.split('/')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function createProductCover(name: string, sku: string) {
|
||||
@@ -80,31 +82,15 @@ function createProductCover(name: string, sku: string) {
|
||||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
function parseProductMeta(product: ProductNode) {
|
||||
const normalizedName = normalizeText(product.name);
|
||||
const [typeLabelRaw] = normalizedName.split(',');
|
||||
const typeLabel = typeLabelRaw?.trim() || 'Без типа';
|
||||
|
||||
const sizeMatch = normalizedName.match(/(\d+)\s*[xх*]\s*(\d+)\s*м/i);
|
||||
const thicknessMatch = normalizedName.match(/(\d+)\s*мкм/i);
|
||||
const sleeveMatch = normalizedName.match(/втулка\s+([^,]+)/i);
|
||||
const quantityMatch = normalizedName.match(/короб\s+([^,]+)/i);
|
||||
const quantityPerBox = quantityMatch?.[1]?.trim() ?? null;
|
||||
|
||||
function hydrateProduct(product: ProductNode): ParsedProduct {
|
||||
return {
|
||||
productTypeLabel: typeLabel,
|
||||
widthMm: sizeMatch ? Number.parseInt(sizeMatch[1] ?? '', 10) : null,
|
||||
lengthM: sizeMatch ? Number.parseInt(sizeMatch[2] ?? '', 10) : null,
|
||||
thicknessMicron: thicknessMatch ? Number.parseInt(thicknessMatch[1] ?? '', 10) : null,
|
||||
sleeveBrand: sleeveMatch?.[1]?.trim() ?? null,
|
||||
quantityPerBox,
|
||||
quantityPerBoxOptions: quantityPerBox
|
||||
? quantityPerBox.split('/').map((value) => value.trim()).filter(Boolean)
|
||||
: [],
|
||||
...product,
|
||||
productTypeLabel: normalizeText(product.productType) || 'Без типа',
|
||||
quantityPerBoxOptions: splitBoxValues(product.quantityPerBox),
|
||||
};
|
||||
}
|
||||
|
||||
function productSortValue(value: number | null) {
|
||||
function productSortValue(value: number | null | undefined) {
|
||||
return value ?? Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
@@ -124,7 +110,7 @@ function compareProducts(a: ParsedProduct, b: ParsedProduct) {
|
||||
return byThickness;
|
||||
}
|
||||
|
||||
const bySleeve = (a.sleeveBrand ?? '').localeCompare(b.sleeveBrand ?? '', 'ru');
|
||||
const bySleeve = normalizeText(a.sleeveBrand).localeCompare(normalizeText(b.sleeveBrand), 'ru');
|
||||
if (bySleeve !== 0) {
|
||||
return bySleeve;
|
||||
}
|
||||
@@ -137,13 +123,22 @@ const parsedProducts = computed<ParsedProduct[]>(() => {
|
||||
const query = search.value.trim().toLowerCase();
|
||||
|
||||
return list
|
||||
.map((product) => ({ ...product, ...parseProductMeta(product) }))
|
||||
.map(hydrateProduct)
|
||||
.filter((product) => {
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
return [product.name, product.sku, product.productTypeLabel, product.sleeveBrand ?? '']
|
||||
.some((part) => part.toLowerCase().includes(query));
|
||||
|
||||
return [
|
||||
product.name,
|
||||
product.sku,
|
||||
product.productTypeLabel,
|
||||
String(product.widthMm ?? ''),
|
||||
String(product.lengthM ?? ''),
|
||||
String(product.thicknessMicron ?? ''),
|
||||
normalizeText(product.sleeveBrand),
|
||||
normalizeText(product.quantityPerBox),
|
||||
].some((part) => part.toLowerCase().includes(query));
|
||||
})
|
||||
.sort(compareProducts);
|
||||
});
|
||||
@@ -152,12 +147,11 @@ const productGroups = computed<ProductGroup[]>(() => {
|
||||
const map = new Map<string, ParsedProduct[]>();
|
||||
|
||||
for (const product of parsedProducts.value) {
|
||||
const key = product.productTypeLabel;
|
||||
const existing = map.get(key);
|
||||
const existing = map.get(product.productTypeLabel);
|
||||
if (existing) {
|
||||
existing.push(product);
|
||||
} else {
|
||||
map.set(key, [product]);
|
||||
map.set(product.productTypeLabel, [product]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,25 +164,6 @@ const productGroups = computed<ProductGroup[]>(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
function matchesState(product: ParsedProduct, state: GroupState, skipField?: ParamFieldKey) {
|
||||
for (const key of PARAM_KEYS) {
|
||||
if (key === skipField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const selectedValue = state[key];
|
||||
if (selectedValue === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (product[key] !== selectedValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function sortParamValues(values: ParamValue[]) {
|
||||
return [...values].sort((a, b) => {
|
||||
if (typeof a === 'number' && typeof b === 'number') {
|
||||
@@ -203,7 +178,7 @@ function getAllFieldOptions(group: ProductGroup, field: ParamFieldKey) {
|
||||
|
||||
for (const product of group.products) {
|
||||
const value = product[field];
|
||||
if (value !== null) {
|
||||
if (value !== null && value !== undefined) {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
@@ -211,17 +186,22 @@ function getAllFieldOptions(group: ProductGroup, field: ParamFieldKey) {
|
||||
return sortParamValues([...values]);
|
||||
}
|
||||
|
||||
function createGroupState(group: ProductGroup): GroupState {
|
||||
void group;
|
||||
const state: GroupState = {
|
||||
function visibleFields(group: ProductGroup) {
|
||||
return parameterFields.filter((field) => getAllFieldOptions(group, field.key).length > 1);
|
||||
}
|
||||
|
||||
function requiredKeys(group: ProductGroup) {
|
||||
return visibleFields(group).map((field) => field.key);
|
||||
}
|
||||
|
||||
function createGroupState(): GroupState {
|
||||
return {
|
||||
widthMm: null,
|
||||
lengthM: null,
|
||||
thicknessMicron: null,
|
||||
sleeveBrand: null,
|
||||
isExpanded: false,
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -236,70 +216,97 @@ watch(
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
if (!groupStates[group.key]) {
|
||||
groupStates[group.key] = createGroupState(group);
|
||||
}
|
||||
groupStates[group.key] ??= createGroupState();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function getGroupState(group: ProductGroup): GroupState {
|
||||
const existing = groupStates[group.key];
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const created = createGroupState(group);
|
||||
groupStates[group.key] = created;
|
||||
return created;
|
||||
function getGroupState(group: ProductGroup) {
|
||||
groupStates[group.key] ??= createGroupState();
|
||||
return groupStates[group.key];
|
||||
}
|
||||
|
||||
function updateField(group: ProductGroup, field: ParamFieldKey, value: ParamValue) {
|
||||
function matchesProductState(product: ParsedProduct, state: GroupState, keys: ParamFieldKey[]) {
|
||||
return keys.every((key) => state[key] === null || product[key] === state[key]);
|
||||
}
|
||||
|
||||
function matchingProducts(group: ProductGroup) {
|
||||
const state = getGroupState(group);
|
||||
state[field] = value as GroupState[typeof field];
|
||||
return group.products.filter((product) => matchesProductState(product, state, requiredKeys(group)));
|
||||
}
|
||||
|
||||
function selectedProduct(group: ProductGroup) {
|
||||
const keys = requiredKeys(group);
|
||||
const state = getGroupState(group);
|
||||
if (PARAM_KEYS.some((key) => state[key] === null)) {
|
||||
|
||||
if (keys.length === 0) {
|
||||
return group.products.length === 1 ? group.products[0] : null;
|
||||
}
|
||||
|
||||
if (keys.some((key) => state[key] === null)) {
|
||||
return null;
|
||||
}
|
||||
return group.products.find((product) => matchesState(product, state)) ?? null;
|
||||
|
||||
const matches = group.products.filter((product) => matchesProductState(product, state, keys));
|
||||
return matches.length === 1 ? matches[0] : null;
|
||||
}
|
||||
|
||||
function matchingCount(group: ProductGroup) {
|
||||
function remainingSelectionCount(group: ProductGroup) {
|
||||
const state = getGroupState(group);
|
||||
return group.products.filter((product) => matchesState(product, state)).length;
|
||||
return requiredKeys(group).filter((key) => state[key] === null).length;
|
||||
}
|
||||
|
||||
function toggleExpanded(group: ProductGroup) {
|
||||
const state = getGroupState(group);
|
||||
state.isExpanded = !state.isExpanded;
|
||||
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 formatOptionLabel(field: ParamFieldKey, value: ParamValue) {
|
||||
if (field === 'widthMm' || field === 'lengthM') {
|
||||
return `${value} мм`;
|
||||
function selectionHeadline(group: ProductGroup) {
|
||||
const product = selectedProduct(group);
|
||||
if (product) {
|
||||
return `SKU ${product.sku}`;
|
||||
}
|
||||
if (field === 'thicknessMicron') {
|
||||
return `${value} мкм`;
|
||||
|
||||
const remaining = remainingSelectionCount(group);
|
||||
if (remaining > 0) {
|
||||
return `Выберите еще ${remaining} ${pluralize(remaining, 'параметр', 'параметра', 'параметров')}`;
|
||||
}
|
||||
return String(value);
|
||||
|
||||
return 'Комбинация не найдена';
|
||||
}
|
||||
|
||||
function selectedFieldValue(group: ProductGroup, field: ParamFieldKey) {
|
||||
const value = getGroupState(group)[field];
|
||||
if (value === null) {
|
||||
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(' • ');
|
||||
}
|
||||
return formatOptionLabel(field, value);
|
||||
|
||||
const remaining = remainingSelectionCount(group);
|
||||
if (remaining > 0) {
|
||||
return 'Переключатели ниже собирают точную модификацию товара.';
|
||||
}
|
||||
|
||||
return 'Разверните весь список, если нужна ручная проверка вариантов.';
|
||||
}
|
||||
|
||||
function boxQuantityLabel(group: ProductGroup) {
|
||||
const product = selectedProduct(group);
|
||||
if (product?.quantityPerBox) {
|
||||
return product.quantityPerBox;
|
||||
if (product?.quantityPerBoxOptions.length) {
|
||||
return product.quantityPerBoxOptions.join(' / ');
|
||||
}
|
||||
|
||||
const values = new Set<string>();
|
||||
@@ -312,6 +319,49 @@ function boxQuantityLabel(group: ProductGroup) {
|
||||
return sortParamValues([...values]).join(' / ') || '—';
|
||||
}
|
||||
|
||||
function formatOptionLabel(field: ParamFieldKey, value: ParamValue) {
|
||||
if (field === 'widthMm') {
|
||||
return `${value} мм`;
|
||||
}
|
||||
if (field === 'lengthM') {
|
||||
return `${value} м`;
|
||||
}
|
||||
if (field === 'thicknessMicron') {
|
||||
return `${value} мкм`;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function isOptionAvailable(group: ProductGroup, field: ParamFieldKey, option: ParamValue) {
|
||||
const state = getGroupState(group);
|
||||
const scopedState = {
|
||||
...state,
|
||||
[field]: option,
|
||||
} satisfies GroupState;
|
||||
|
||||
return group.products.some((product) => matchesProductState(product, scopedState, requiredKeys(group)));
|
||||
}
|
||||
|
||||
function updateField(group: ProductGroup, field: ParamFieldKey, value: ParamValue) {
|
||||
const state = getGroupState(group);
|
||||
state[field] = value as GroupState[typeof field];
|
||||
}
|
||||
|
||||
function clearField(group: ProductGroup, field: ParamFieldKey) {
|
||||
getGroupState(group)[field] = null;
|
||||
}
|
||||
|
||||
function clearSelection(group: ProductGroup) {
|
||||
const state = getGroupState(group);
|
||||
for (const key of PARAM_KEYS) {
|
||||
state[key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExpanded(group: ProductGroup) {
|
||||
getGroupState(group).isExpanded = !getGroupState(group).isExpanded;
|
||||
}
|
||||
|
||||
function incrementProduct(product: ProductNode) {
|
||||
if (getQuantity(product.id) === 0) {
|
||||
addProduct({
|
||||
@@ -332,26 +382,21 @@ function decrementProduct(productId: string) {
|
||||
|
||||
function selectedQty(group: ProductGroup) {
|
||||
const product = selectedProduct(group);
|
||||
if (!product) {
|
||||
return 0;
|
||||
}
|
||||
return getQuantity(product.id);
|
||||
return product ? getQuantity(product.id) : 0;
|
||||
}
|
||||
|
||||
function incrementSelected(group: ProductGroup) {
|
||||
const product = selectedProduct(group);
|
||||
if (!product) {
|
||||
return;
|
||||
if (product) {
|
||||
incrementProduct(product);
|
||||
}
|
||||
incrementProduct(product);
|
||||
}
|
||||
|
||||
function decrementSelected(group: ProductGroup) {
|
||||
const product = selectedProduct(group);
|
||||
if (!product) {
|
||||
return;
|
||||
if (product) {
|
||||
decrementProduct(product.id);
|
||||
}
|
||||
decrementProduct(product.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -380,76 +425,110 @@ function decrementSelected(group: ProductGroup) {
|
||||
:key="group.key"
|
||||
class="surface-card rounded-3xl p-4 md:p-5"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<h2 class="text-xl font-bold text-[#133826]">{{ group.typeLabel }}</h2>
|
||||
<span class="badge badge-outline">{{ group.products.length }} вариантов</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-4 xl:grid-cols-[360px_1fr]">
|
||||
<aside class="rounded-2xl bg-base-100 p-3">
|
||||
<div class="grid gap-4 xl:grid-cols-6 xl:items-stretch">
|
||||
<div class="rounded-[28px] bg-base-100 p-3 xl:col-span-1">
|
||||
<img
|
||||
:src="createProductCover(group.typeLabel, group.key)"
|
||||
:alt="`Превью группы ${group.typeLabel}`"
|
||||
class="h-56 w-full rounded-2xl object-cover"
|
||||
class="h-full min-h-[220px] w-full rounded-[24px] object-cover"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<span class="badge badge-neutral">SKU: {{ selectedProduct(group)?.sku ?? '—' }}</span>
|
||||
<span class="badge badge-outline">Совпадений: {{ matchingCount(group) }}</span>
|
||||
<span class="badge badge-outline">Короб: {{ boxQuantityLabel(group) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center justify-between rounded-2xl border border-base-300 bg-base-100 px-2 py-1">
|
||||
<button
|
||||
class="btn btn-square btn-sm"
|
||||
:disabled="selectedQty(group) === 0"
|
||||
@click="decrementSelected(group)"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span class="min-w-10 text-center font-semibold">{{ selectedQty(group) }}</span>
|
||||
<button class="btn btn-square btn-sm" @click="incrementSelected(group)">+</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="field in parameterFields"
|
||||
:key="`selected-${group.key}-${field.key}`"
|
||||
class="badge badge-outline"
|
||||
>
|
||||
{{ field.label }}: {{ selectedFieldValue(group, field.key) }}
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid gap-3 md:grid-cols-2 2xl:grid-cols-3">
|
||||
<div
|
||||
v-for="field in parameterFields"
|
||||
<div class="rounded-[28px] bg-base-100 p-4 md:p-5 xl:col-span-4">
|
||||
<div class="grid h-full gap-4 md:grid-cols-2">
|
||||
<fieldset
|
||||
v-for="field in visibleFields(group)"
|
||||
:key="`${group.key}-${field.key}`"
|
||||
class="rounded-2xl bg-base-100 p-3"
|
||||
class="rounded-[24px] bg-base-200/50 p-4"
|
||||
>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/60">{{ field.label }}</p>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<legend class="text-sm font-semibold text-[#163624]">{{ field.label }}</legend>
|
||||
<button
|
||||
v-for="option in getAllFieldOptions(group, field.key)"
|
||||
:key="`${group.key}-${field.key}-${option}`"
|
||||
class="btn btn-sm"
|
||||
:class="getGroupState(group)[field.key] === option ? 'btn-neutral' : 'btn-outline'"
|
||||
@click="updateField(group, field.key, option)"
|
||||
v-if="getGroupState(group)[field.key] !== null"
|
||||
class="btn btn-ghost btn-xs rounded-full px-2"
|
||||
@click="clearField(group, field.key)"
|
||||
>
|
||||
{{ formatOptionLabel(field.key, option) }}
|
||||
Сбросить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<input
|
||||
v-for="option in getAllFieldOptions(group, field.key)"
|
||||
:key="`${group.key}-${field.key}-${option}`"
|
||||
class="btn btn-sm rounded-full border-base-300 bg-base-100 text-sm normal-case checked:btn-neutral"
|
||||
type="radio"
|
||||
:name="`${group.key}-${field.key}`"
|
||||
:aria-label="formatOptionLabel(field.key, option)"
|
||||
:checked="getGroupState(group)[field.key] === option"
|
||||
:disabled="!isOptionAvailable(group, field.key, option)"
|
||||
@change="updateField(group, field.key, option)"
|
||||
>
|
||||
</div>
|
||||
</fieldset>
|
||||
</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>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-base-content/45">Тип товара</p>
|
||||
<h2 class="mt-2 text-xl font-bold text-[#163624]">{{ group.typeLabel }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="badge badge-outline">{{ group.products.length }} вариантов</span>
|
||||
<span class="badge badge-outline">Короб: {{ boxQuantityLabel(group) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[22px] bg-base-200/70 p-3">
|
||||
<p class="text-sm font-semibold text-[#163624]">{{ selectionHeadline(group) }}</p>
|
||||
<p class="mt-1 text-xs leading-5 text-base-content/65">{{ selectionDescription(group) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div 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"
|
||||
:disabled="selectedQty(group) === 0"
|
||||
@click="decrementSelected(group)"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<div class="text-center">
|
||||
<div class="text-[11px] uppercase tracking-[0.16em] text-base-content/45">В корзине</div>
|
||||
<div class="text-lg font-semibold text-[#163624]">{{ selectedQty(group) }}</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-square btn-sm"
|
||||
:disabled="!selectedProduct(group)"
|
||||
@click="incrementSelected(group)"
|
||||
>
|
||||
+
|
||||
</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>
|
||||
|
||||
<p class="text-sm text-base-content/70">Совпадающих вариантов: {{ matchingCount(group) }}</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div v-if="getGroupState(group).isExpanded" class="mt-4 overflow-x-auto rounded-2xl bg-base-100 p-2">
|
||||
<div
|
||||
v-if="getGroupState(group).isExpanded"
|
||||
class="mt-4 overflow-x-auto rounded-[28px] bg-base-100 p-2"
|
||||
>
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -481,7 +560,13 @@ function decrementSelected(group: ProductGroup) {
|
||||
<td>{{ product.quantityPerBox ?? '—' }}</td>
|
||||
<td class="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" :disabled="getQuantity(product.id) === 0" @click="decrementProduct(product.id)">-</button>
|
||||
<button
|
||||
class="btn btn-xs btn-square"
|
||||
:disabled="getQuantity(product.id) === 0"
|
||||
@click="decrementProduct(product.id)"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span class="text-sm font-semibold">{{ getQuantity(product.id) }}</span>
|
||||
<button class="btn btn-xs btn-square" @click="incrementProduct(product)">+</button>
|
||||
</div>
|
||||
@@ -502,7 +587,13 @@ function decrementSelected(group: ProductGroup) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M5 7.5L10 12.5L15 7.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M5 7.5L10 12.5L15 7.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ getGroupState(group).isExpanded ? 'Свернуть все варианты' : 'Развернуть все варианты' }}</span>
|
||||
</button>
|
||||
|
||||
@@ -404,8 +404,14 @@ export type Product = {
|
||||
id: Scalars['ID']['output'];
|
||||
isActive: Scalars['Boolean']['output'];
|
||||
isCustomizable: Scalars['Boolean']['output'];
|
||||
lengthM?: Maybe<Scalars['Int']['output']>;
|
||||
name: Scalars['String']['output'];
|
||||
productType?: Maybe<Scalars['String']['output']>;
|
||||
quantityPerBox?: Maybe<Scalars['String']['output']>;
|
||||
sku: Scalars['String']['output'];
|
||||
sleeveBrand?: Maybe<Scalars['String']['output']>;
|
||||
thicknessMicron?: Maybe<Scalars['Int']['output']>;
|
||||
widthMm?: Maybe<Scalars['Int']['output']>;
|
||||
};
|
||||
|
||||
export type ProductWarehouseBalance = {
|
||||
@@ -639,7 +645,7 @@ export type VerifyLoginCodeMutation = { __typename?: 'Mutation', verifyLoginCode
|
||||
export type ClientProductsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ClientProductsQuery = { __typename?: 'Query', clientProducts: Array<{ __typename?: 'Product', id: string, sku: string, name: string, description?: string | null, isCustomizable: boolean, availableInWarehouses: Array<{ __typename?: 'ProductWarehouseBalance', availableQty: number, warehouse: { __typename?: 'Warehouse', id: string, code: string, name: string } }> }> };
|
||||
export type ClientProductsQuery = { __typename?: 'Query', clientProducts: Array<{ __typename?: 'Product', id: string, sku: string, name: string, description?: string | null, productType?: string | null, widthMm?: number | null, lengthM?: number | null, thicknessMicron?: number | null, sleeveBrand?: string | null, quantityPerBox?: string | null, isCustomizable: boolean, availableInWarehouses: Array<{ __typename?: 'ProductWarehouseBalance', availableQty: number, warehouse: { __typename?: 'Warehouse', id: string, code: string, name: string } }> }> };
|
||||
|
||||
export type MyMessengerConnectionsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -920,6 +926,12 @@ export const ClientProductsDocument = gql`
|
||||
sku
|
||||
name
|
||||
description
|
||||
productType
|
||||
widthMm
|
||||
lengthM
|
||||
thicknessMicron
|
||||
sleeveBrand
|
||||
quantityPerBox
|
||||
isCustomizable
|
||||
availableInWarehouses {
|
||||
availableQty
|
||||
|
||||
Reference in New Issue
Block a user