PageSourceSearch

https://www.mirkrasok.ru/local/templates/mirkrasok_2026v2/componen…/js/card-logic.js?v=1789766231

js mirkrasok.ru collected 2026-10-03 20:56:59 UTC 88,192 bytes, 1,857 lines download raw bytes

1/**
2 * Статичная логика карточки товара (catalog.element.claude).
3 *
4 * Здесь лежат methods, computed, хуки и watch Vue-компонента product-detail.
5 * Код одинаков для любого товара и не содержит серверных вставок, поэтому
6 * вынесен из inline-скрипта в отдельный файл: браузер кеширует его и не
7 * разбирает заново на каждой карточке.
8 *
9 * Динамика — data с данными товара и сам шаблон — осталась в template.php.
10 *
11 * Файл подключается с ?v=filemtime, поэтому после правок кеш сбрасывается сам.
12 */
13
14function pluralForm(n, forms) {
15    return n % 10 == 1 && n % 100 != 11 ? forms[0] : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? forms[1] : forms[2]);
16}
17
18var tobasket = 0;
19
20// Оптимизация: debounce для частых вызовов
21function debounce(fn, delay) {
22    let timer = null;
23    return function(...args) {
24        clearTimeout(timer);
25        timer = setTimeout(() => fn.apply(this, args), delay);
26    };
27}
28
29// Оптимизация: кэш для запросов
30const requestCache = new Map();
31const CACHE_TTL = 300000; // 5 минут
32
33// Разметка списка отзывов: сервер отдаёт её скрытым блоком в обычном DOM (ради
34// индексации). Таб пересоздаётся через v-if, поэтому держим разметку здесь и
35// подставляем заново при каждом открытии вкладки.
36let reviewsHtmlCache = null;
37
38// Характеристики сервер отдаёт скрытым блоком в обычном DOM (ради индексации).
39// Забираем разметку сюда один раз и подставляем в таб при каждом открытии.
40let techHtmlCache = null;
41
42export const cardOptions = {
43            methods: {
44                // Оптимизация: вычисление summarRRC вынесено в метод с кэшированием
45                calculateSummarRRC() {
46                    let summarRRC = 0;
47                    let isSumelement = true;
48
49                    if (this.basketItems) {
50                        for (let key in this.basketItems) {
51                            if (this.basketItems[key].ID == this.currentSku?.ID) {
52                                isSumelement = false;
53                            }
54                            summarRRC += this.basketItems[key].PRICE_RRC * this.basketItems[key].QUANTITY;
55                        }
56                    }
57
58                    if (isSumelement && this.currentSku?.PRICE_MATRIX?.RRC?.DISCOUNT_VALUE && this.dataSkuJs[this.currentSku.ID]) {
59                        summarRRC += this.currentSku.PRICE_MATRIX.RRC.DISCOUNT_VALUE * (this.dataSkuJs[this.currentSku.ID].QUANTITY_DEFAULT - 1);
60                    }
61
62                    return summarRRC;
63                },
64
65                // Оптимизация: getSkuDataPrice с защитой от повторного вызова
66                getSkuDataPrice() {
67                    if (this.isUpdating || !this.currentSku) return;
68
69                    const data = [];
70                    const sku = this.currentSku;
71
72                    this.priceLine.forEach((price) => {
73                        const priceType = price.CODE;
74                        const priceNew = sku.new_price_set?.[priceType];
75                        if (!priceNew) return;
76
77                        if (price.ID == 40 && ['TIKKURILA', 'FINNCOLOR Россия', 'ТЕКС Россия'].includes(this.brand.NAME)) {
78                            return;
79                        }
80
81                        let isHidden = sku.MARKETING && priceType !== 'RRC';
82                        let requestPrice = sku.REQUEST_PRICES?.includes(priceType) && !this.brand.SHOW_ALL_PRICES;
83
84                        let oldPrice, currentPrice, iterator, classPrice;
85
86                        if ((sku.SHOW_YM_MRC && !requestPrice) && priceNew.NEW_VALUE < priceNew.OLD_VALUE) {
87                            oldPrice = priceNew.OLD_VALUE;
88                            currentPrice = priceNew.NEW_VALUE;
89                            isHidden = false;
90                            iterator = 1;
91                            classPrice = 'new_price';
92                        } else {
93                            const skuPriceRrcOld = parseFloat(sku.new_price_set?.RRC_OLD?.VALUE || 0);
94                            if (parseInt(sku.MARKETING?.DISCOUNTNTH) !== 1) {
95                                oldPrice = skuPriceRrcOld > parseFloat(priceNew.VALUE) ? skuPriceRrcOld : null;
96                                currentPrice = priceNew.VALUE;
97                                iterator = 2;
98                                classPrice = 'price';
99                            } else {
100                                oldPrice = skuPriceRrcOld;
101                                currentPrice = priceNew.VALUE;
102                                iterator = 3;
103                                classPrice = 'new_price';
104                            }
105                        }
106
107                        data.push({
108                            old_price: oldPrice,
109                            price: currentPrice,
110                            hidden: isHidden,
111                            id: price.ID,
112                            priceType,
113                            iterator,
114                            class_price: classPrice,
115                            bonus_price: priceNew.BONUS_RUB,
116                            price_cancel: isHidden ? 'set-price-cancel' : ''
117                        });
118                    });
119
120                    this.dataPrice = data;
121
122                    // Оптимизация: отложенное обновление DOM
123                    this.$nextTick(() => {
124                        this.updatePriceLine();
125                    });
126                },
127
128                // Оптимизация: updatePriceLine с защитой
129                updatePriceLine() {
130                    if (this.isUpdating) return;
131
132                    this.$nextTick(() => {
133                        $(".info-box-card-price [data-price-id]").each(function (index, el) {
134                            if ($(el).find(".new_price, .price").hasClass('set-price-cancel')) {
135                                $(el).removeClass("price_active").addClass("price_hidden");
136                                $(el).parent().find("[data-price-id]:first").addClass("price_active").removeClass("price_hidden");
137                            }
138                        });
139                    });
140                },
141
142                // Оптимизация: getDiscontLable без forceUpdate
143                getDiscontLable(sku) {
144                    if (!sku?.MARKETING) return false;
145
146                    const RRC = sku.new_price_set?.RRC;
147                    if (!RRC) return false;
148
149                    const value = Math.floor((1 - RRC.NEW_VALUE / RRC.OLD_VALUE).toFixed(2) * 100);
150                    const title = Object.values(sku.MARKETING)[0].TITLE;
151                    const text = title.includes(value) ? title : value + "% " + title;
152                    const smallLable = value ? "-" + value + "%" : "%";
153
154                    return {
155                        smallLable,
156                        text,
157                        link: Object.values(sku.MARKETING)[0].URL
158                    };
159                },
160
161                getCurrentDiscontLable() {
162                    const discont = this.getDiscontLable(this.currentSku);
163                    this.visibleMainMarketing = discont;
164                    return discont;
165                },
166
167                // Оптимизация: requestReviews с кэшированием
168                async requestReviews() {
169                    if (this.reviewsRequested) return;
170                    this.reviewsRequested = true;
171
172                    const cacheKey = `reviews_${this.PRODUCT_ID}`;
173                    const cached = requestCache.get(cacheKey);
174
175                    if (cached && (Date.now() - cached.timestamp < CACHE_TTL)) {
176                        this.reviews = cached.data;
177                        return;
178                    }
179
180                    try {
181                        const response = await axios.post(
182                            "/local/php_interface/ajax/ajaxReviewsJson.php?clear_cache=Y",
183                            { PRODUCT_ID: this.PRODUCT_ID }
184                        );
185
186                        requestCache.set(cacheKey, {
187                            data: response.data,
188                            timestamp: Date.now()
189                        });
190
191                        this.reviews = response.data;
192                    } catch (e) {
193                        this.reviewsRequested = false;
194                        console.log("Не загрузились отзывы");
195                    }
196                },
197
198                // Оптимизация: обработчики с debounce
199                handleScroll: debounce(function(event) {
200                    this.scroll = window.scrollY;
201
202                    const element = document.querySelector('.product-box');
203                    if (element) {
204                        const rect = element.getBoundingClientRect();
205                        this.scrollMAX = rect.bottom + window.pageYOffset - 600;
206                    }
207
208                    this.iOffset = window.scrollY || document.documentElement.scrollTop;
209                }, 16),
210
211                handleResize: debounce(function() {
212                    this.isSmallScreen = window.innerWidth <= 1200;
213                    this.isIPad = window.innerWidth >= 1100 && window.innerWidth <= 1200;
214                }, 100),
215
216                // Остальные методы без изменений
217                getSkuQuantity(quantity, minus = false) {
218                    const item = this.dataSkuJs[this.currentSku.ID];
219                    if (minus) {
220                        item.QUANTITY_DEFAULT = Math.max(item.QUANTITY_DEFAULT - quantity, 1);
221                    } else {
222                        item.QUANTITY_DEFAULT += quantity;
223                    }
224                },
225
226                getCurrentPrice() {
227                    if (this.currentSku?.MARKETING) {
228                        return this.currentSku?.new_price_set?.RRC?.NEW_VALUE;
229                    }
230                    return this.currentSku?.new_price_set?.[this.getCurrentPriceType]?.NEW_VALUE;
231                },
232
233                getSumDiscount() {
234                    var discount = (this.currentSku?.new_price_set?.[this.getCurrentPriceType]?.OLD_VALUE || 0) - (this.getCurrentPrice() || 0);
235                    if(discount==0){
236                        discount = this.currentSku?.new_price_set.RRC.OLD_VALUE-this.getCurrentPrice();
237                    }
238                    return (discount);
239                },
240
241                showPopup() {
242                    this.img.showPopup = true;
243                    $('#main .container .header_wrapper').addClass('lower');
244                },
245
246                initSlider() {
247                    const $slider = $('.img-box [data-js="slick-slider"]');
248                    if (!$slider.length) return;
249
250                    $slider.slick({
251                        dots: true,
252                        arrows: false,
253                        autoplay: false,
254                        infinite: true,
255                        slidesToShow: 1,
256                        slidesToScroll: 1,
257                        lazyLoad: 'ondemand',
258                        // Своя ARIA-разметка slick не проходит проверку доступности
259                        // (role=tablist с недопустимыми детьми, aria-selected на li,
260                        // aria-hidden поверх ссылок). Листание с клавиатуры и мышью
261                        // при этом не меняется.
262                        accessibility: false
263                    });
264
265                    // Точки переключения — кнопки без текста, подписываем их сами.
266                    // Заодно снимаем aria-hidden, который slick ставит неактивным
267                    // точкам: кнопка внутри остаётся доступной с клавиатуры, и
268                    // получается элемент, спрятанный от чтения, но достижимый табом.
269                    const fixDots = () => {
270                        $slider.find('.slick-dots li').each(function (index) {
271                            this.removeAttribute('aria-hidden');
272                            const button = this.querySelector('button');
273                            if (button) {
274                                button.setAttribute('aria-label', 'Фото ' + (index + 1));
275                            }
276                        });
277                    };
278                    fixDots();
279                    $slider.on('afterChange setPosition', fixDots);
280
281                    $slider.on('afterChange', (event, slick, currentSlide) => {
282                        const images = this.getColectionImg;
283                        if (images[currentSlide]) {
284                            this.selectImg(images[currentSlide]);
285                        }
286                    });
287                },
288
289                updateBasket(ELEMENT_ID, QUANTITY) {
290                    const smallBasket = $('.head_basket_drop form');
291                    const currentItem = smallBasket.find(`[data-product-id=${ELEMENT_ID}]`);
292
293                    if (currentItem.is('tr')) {
294                        currentItem.find('[data-quantity]').val(QUANTITY);
295                    } else if (currentItem.is('input')) {
296                        currentItem.val(QUANTITY);
297                    }
298
299                    if (QUANTITY > 0) {
300                        this.currentBasketItem.QUANTITY = QUANTITY;
301                        this.currentBasketItem.PRICE_RRC = $(currentItem).data("pricerrc");
302                    } else {
303                        this.basketItems = this.basketItems?.filter((item) => item.ID !== this.currentBasketItem.ID);
304                        this.currentBasketItem = this.inBasket();
305                        currentItem.remove();
306                    }
307
308                    obBasket.refreshBasket(smallBasket[0]);
309                },
310
311                inBasket() {
312                    return this.basketItems?.find((item) => item.ID === this.currentSku?.ID);
313                },
314
315                toggleBlock(blockNumber) {
316                    this.showBlock[blockNumber] = !this.showBlock[blockNumber];
317                },
318
319                addCompare() {},
320
321                addToWishList() {
322                    obWishList.click(this);
323                },
324
325                showShopCount(ELEMENT_ID) {
326                    obDetailProduct.showQuantity(ELEMENT_ID);
327                },
328
329                pushEcommerceDetail() {
330                    window.dataLayer = window.dataLayer || [];
331                    dataLayer.push({
332                        'ecommerce': {
333                            'currencyCode': 'RUB',
334                            'detail': {
335                                'products': [this.currentSku?.GTM],
336                            },
337                        },
338                        'goods_id': this.currentSku?.ID,
339                        'goods_price': this.currentSku?.new_price_set?.RRC?.VALUE,
340                        'page_type': 'product',
341                        'event': 'pixel-mg-event',
342                        'pixel-mg-event-category': 'Enhanced Ecommerce',
343                        'pixel-mg-event-action': 'Product Details',
344                        'pixel-mg-event-non-interaction': 'True'
345                    });
346                },
347
348                pushEcommerceAddBasket(currentSku, QUANTITY) {
349                    window.dataLayer = window.dataLayer || [];
350                    if (!QUANTITY) QUANTITY = 1;
351                    currentSku.GTM.quantity = QUANTITY;
352                    dataLayer.push({
353                        "ecommerce": {
354                            'currencyCode': 'RUB',
355                            "add": {
356                                "products": [{
357                                    "id": currentSku.ID,
358                                    "name": currentSku.GTM.name,
359                                    "brand": currentSku.GTM.brand,
360                                    "category": currentSku.GTM.category,
361                                    "price": currentSku.GTM.price,
362                                    "variant": currentSku.GTM.variant,
363                                    "quantity": currentSku.GTM.quantity
364                                }],
365                            }
366                        },
367                        'goods_id': currentSku.ID,
368                        'goods_price': currentSku.GTM.price,
369                        'page_type': 'add_product',
370                        'event': 'pixel-mg-event',
371                        'pixel-mg-event-category': 'Enhanced Ecommerce',
372                        'pixel-mg-event-action': 'Adding a Product to a Shopping Cart',
373                        'pixel-mg-event-non-interaction': 'False'
374                    });
375                },
376
377                pushEcommerceRemove(currentSku, QUANTITY) {
378                    window.dataLayer = window.dataLayer || [];
379                    if (!QUANTITY) QUANTITY = 1;
380                    currentSku.GTM.quantity = QUANTITY;
381                    dataLayer.push({
382                        "ecommerce": {
383                            "currencyCode": "RUB",
384                            "remove": {
385                                "products": [{
386                                    "id": currentSku.ID,
387                                    "name": currentSku.GTM.name,
388                                    "brand": currentSku.GTM.brand,
389                                    "category": currentSku.GTM.category,
390                                    "price": currentSku.GTM.price,
391                                    "variant": currentSku.GTM.variant,
392                                    "quantity": currentSku.GTM.quantity
393                                }]
394                            }
395                        },
396                        'goods_id': currentSku.ID,
397                        'goods_price': currentSku.GTM.price,
398                        "page_type": "add_product",
399                        "event": "pixel-mg-event",
400                        "pixel-mg-event-category": "Enhanced Ecommerce",
401                        "pixel-mg-event-action": "Adding a Product to a Shopping Cart",
402                        "pixel-mg-event-non-interaction": "False"
403                    });
404                },
405
406                addToBasket(ELEMENT_ID, QUANTITY, options = {}) {
407                    var MF_COLOR = $('.MF_COLOR .property-filter-value.__active:first').index();
408                    tobasket = 1;
409
410                    let colorParams = {};
411                    if (selectedColor && selectedColor[MF_COLOR]) {
412                        colorParams = {
413                            COLOR_ID: selectedColor[MF_COLOR].COLOR_ID,
414                            COLOR_SKU_ID: selectedColor[MF_COLOR].COLOR_SKU_ID,
415                            COLLECTION_NAME: selectedColor[MF_COLOR].COLLECTION_NAME,
416                            COLLECTION_ID: selectedColor[MF_COLOR].COLLECTION_ID,
417                            COLOR_NAME: selectedColor[MF_COLOR].COLOR_NAME,
418                            HEX: selectedColor[MF_COLOR].HEX,
419                            VOLUME: selectedColor[MF_COLOR].VOLUME,
420                            COLOR_PRICE: selectedColorPRICE?.[MF_COLOR]
421                        };
422                    }
423
424                    return $.ajax({
425                        type: "POST",
426                        url: "/local/components/art-fresh/sale.basket/ajax_new.php",
427                        data: {
428                            action: "add",
429                            ELEMENT_ID,
430                            QUANTITY,
431                            ...colorParams
432                        },
433                        timeout: 15000,
434                        success: (data) => {
435                            const $data = $(data);
436                            console.log('123456');
437                            console.log($data);
438                            const price = $data.find(`[data-product-id=${ELEMENT_ID}]`).data('price');
439                            const priceRRC = $data.find(`[data-product-id=${ELEMENT_ID}]`).data('pricerrc');
440
441                            obProduct.productAdded(ELEMENT_ID, true);
442                            $('.icon-link.cart-link span').html(data);
443                            $('.link-cart span').html(data);
444                            obProduct.productShowPopup(ELEMENT_ID, false, QUANTITY, price);
445
446                            $("[data-box-basket]").addClass("active");
447                            this.basketItems.push({ ID: ELEMENT_ID, QUANTITY, PRICE_RRC: priceRRC });
448                            this.$nextTick(() => this.applyTintPrice());
449                        },
450                        error: function () {
451                            // Обработка ошибки
452                        }
453                    });
454                },
455
456                getCalcSkuBaseCode(sku) {
457                    const candidates = [
458                        sku?.IB_BASE_ENUM,
459                        sku?.FILTER_PROPS?.IB_BASE?.VALUE_XML_ID,
460                        sku?.FILTER_PROPS?.IB_BASE?.VALUE_ENUM,
461                        sku?.BASE_ENUM,
462                        sku?.WEB_NAME,
463                        sku?.NAME
464                    ];
465
466                    for (const candidate of candidates) {
467                        const value = String(candidate || '').trim().toUpperCase();
468                        if (!value) {
469                            continue;
470                        }
471
472                        const match = value.match(/\b(BW|BC|TR|CLR|A|B|C)\b/);
473                        if (match) {
474                            return match[1];
475                        }
476                    }
477
478                    return '';
479                },
480
481                getCalcSkuVolume(sku) {
482                    const rawVolume = sku?.OBEM_V_L ?? sku?.VOLUME ?? sku?.VES_MATERIALA ?? 0;
483                    const volume = Number(String(rawVolume).replace(',', '.'));
484                    return Number.isFinite(volume) ? volume : 0;
485                },
486
487                resolveCalcBasketProductId(item) {
488                    const requestedProductId = String(item?.product_id || '').trim();
489                    const requestedBase = String(item?.base || '').trim().toUpperCase();
490                    const requestedSize = Number(item?.size || 0);
491
492                    if (requestedProductId) {
493                        const byId = (this.skus || []).find((sku) => String(sku?.ID || '') === requestedProductId);
494                        if (byId?.ID) return String(byId.ID);
495                    }
496
497                    if (!requestedBase || !Number.isFinite(requestedSize) || requestedSize <= 0) {
498                        return '';
499                    }
500
501                    const byBaseAndVolume = (this.skus || []).find((sku) => {
502                        const skuBase = this.getCalcSkuBaseCode(sku);
503                        const skuVolume = this.getCalcSkuVolume(sku);
504                        return skuBase === requestedBase && Math.abs(skuVolume - requestedSize) < 0.0001;
505                    });
506
507                    return byBaseAndVolume?.ID ? String(byBaseAndVolume.ID) : '';
508                },
509
510                getCalcColorPayload() {
511                    const activeIndex = $('.MF_COLOR .property-filter-value.__active:first').index();
512                    const colorIndex = activeIndex >= 0 ? activeIndex : 0;
513                    const colorData = typeof selectedColor !== 'undefined' ? selectedColor[colorIndex] : null;
514                    const colorPrice = typeof selectedColorPRICE !== 'undefined' ? selectedColorPRICE[colorIndex] : null;
515
516                    if (!colorData || typeof colorData !== 'object') return null;
517
518                    return {
519                        COLOR_ID: colorData.COLOR_ID,
520                        COLOR_SKU_ID: colorData.COLOR_SKU_ID,
521                        COLLECTION_NAME: colorData.COLLECTION_NAME,
522                        COLLECTION_ID: colorData.COLLECTION_ID,
523                        COLOR_NAME: colorData.COLOR_NAME,
524                        HEX: colorData.HEX,
525                        VOLUME: colorData.VOLUME,
526                        COLOR_PRICE: colorPrice
527                    };
528                },
529
530                async handleCalcAddToCart(event) {
531                    const detail = event?.detail || {};
532                    const payload = detail.payload || {};
533                    const basketItems = Array.isArray(detail.basketItems) ? detail.basketItems : [];
534                    const colorPayload = this.getCalcColorPayload();
535                    const newBasketItems = [];
536
537                    basketItems.forEach((item) => {
538                        const quantity = Number(item?.quantity || 0);
539                        const productId = this.resolveCalcBasketProductId(item);
540
541                        if (!productId || !Number.isFinite(quantity) || quantity <= 0) return;
542
543                        newBasketItems.push({
544                            PRODUCT_ID: productId,
545                            itemsUsed: quantity
546                        });
547                    });
548
549                    if (!newBasketItems.length) {
550                        const fallbackQuantity = Number(payload?.quantity || 0);
551                        const fallbackProductId = this.resolveCalcBasketProductId({
552                            product_id: payload?.product_id || '',
553                            base: payload?.base || '',
554                            size: null
555                        });
556
557                        if (fallbackProductId && Number.isFinite(fallbackQuantity) && fallbackQuantity > 0) {
558                            newBasketItems.push({
559                                PRODUCT_ID: fallbackProductId,
560                                itemsUsed: fallbackQuantity
561                            });
562                        }
563                    }
564
565                    if (!newBasketItems.length) return;
566
567                    await $.ajax({
568                        type: "POST",
569                        url: "/local/components/art-fresh/sale.basket/ajax_new.php",
570                        data: {
571                            items: newBasketItems,
572                            action: 'calc_basket_add',
573                            BIG_BASKET: 1,
574                            basket_new: 'Y',
575                            sku: (this.skus || []).map(item => item.ID),
576                            ...(colorPayload || {})
577                        },
578                        timeout: 15000,
579                        success: (data) => {
580                            const firstBasketItem = newBasketItems[0] || null;
581                            const firstElementId = firstBasketItem?.PRODUCT_ID || this.currentSku?.ID || 0;
582                            const firstQuantity = Number(firstBasketItem?.itemsUsed || payload?.quantity || 0) || 1;
583                            const price = $(data).find(`[data-product-id=${firstElementId}]`).data('price');
584                            const priceRRC = $(data).find(`[data-product-id=${firstElementId}]`).data('pricerrc');
585
586                            if (firstElementId) {
587                                obProduct.productAdded(firstElementId, true);
588                            }
589
590                            obBasket.changeBasket(data);
591                            $('.icon-link.cart-link span').html(data);
592                            $('.link-cart span').html(data);
593
594                            if (firstElementId) {
595                                obProduct.productShowPopup(firstElementId, false, firstQuantity, price);
596                            }
597
598                            $("[data-box-basket]").addClass("active");
599
600                            newBasketItems.forEach((basketItem) => {
601                                this.basketItems.push({
602                                    ID: basketItem.PRODUCT_ID,
603                                    QUANTITY: basketItem.itemsUsed,
604                                    PRICE_RRC: priceRRC
605                                });
606                            });
607                        }
608                    });
609
610                    this.closeCalcIframePopup();
611                },
612
613                openCalcIframe() {
614                    const trigger = document.getElementById('openCalcIframe');
615                    if (!trigger) return;
616
617                    const plainProducts = JSON.parse(JSON.stringify(this.calcIframe.products || {}));
618                    const currentPriceType = String(this.getCurrentPriceType || this.PriceType?.PRICE_CODE || 'RRC');
619
620                    Object.keys(plainProducts).forEach((baseCode) => {
621                        const product = plainProducts[baseCode];
622                        if (!product || typeof product !== 'object') return;
623
624                        product.line_price = this.PriceType?.AVAILABLE_PRICES || {};
625                        product.selected_price_code = currentPriceType;
626
627                        if (!Array.isArray(product.base_prices)) return;
628
629                        product.base_prices = product.base_prices.map((pack) => {
630                            const volumeKey = String(Number(pack?.volume ?? pack?.size ?? 0));
631                            const productSkuIds = Array.isArray(product.
631sku_ids) ? product.sku_ids.map(String) : [];
632                            const matchedSku = (this.skus || []).find((sku) => {
633                                const skuId = String(sku?.ID || '');
634                                return productSkuIds.includes(skuId) && String(this.getCalcSkuVolume(sku)) === volumeKey;
635                            });
636
637                            const pricesFromSku = matchedSku ? {
638                                RRC: this.getSkuPriceByCode(matchedSku, 'RRC'),
639                                MRC: this.getSkuPriceByCode(matchedSku, 'MRC'),
640                                PRC: this.getSkuPriceByCode(matchedSku, 'PRC'),
641                                BOC1: this.getSkuPriceByCode(matchedSku, 'BOC1'),
642                            } : {};
643
644                            const prices = { ...(pack?.prices || {}), ...pricesFromSku };
645                            const rrcPrice = Number(prices.RRC);
646
647                            return {
648                                ...pack,
649                                prices,
650                                price: Number.isFinite(rrcPrice) && rrcPrice >= 0 ? rrcPrice : pack.price
651                            };
652                        });
653                    });
654
655                    trigger.setAttribute('data-products', JSON.stringify(plainProducts));
656                    if (String(this.currentSku?.ID || '').trim()) {
657                        trigger.setAttribute('data-selected-sku-id', String(this.currentSku.ID).trim());
658                    } else {
659                        trigger.removeAttribute('data-selected-sku-id');
660                    }
661                    trigger.click();
662                },
663
664                getSkuPriceByCode(sku, priceCode) {
665                    const priceData = sku?.new_price_set?.[priceCode];
666                    const priceValue = priceData?.NEW_VALUE ?? priceData?.VALUE ?? sku?.PRICE_MATRIX?.[priceCode]?.VALUE ?? 0;
667                    const price = Number(priceValue);
668                    return Number.isFinite(price) ? price : 0;
669                },
670
671                closeCalcIframePopup() {
672                    const closeBtn = document.getElementById('closeCalcIframe');
673                    if (closeBtn) closeBtn.click();
674                },
675
676                addToRezerv(ELEMENT_ID) {
677                    if (!this.getQuantityShop) return false;
678                    obDetailProduct.showQuantityRezerv(ELEMENT_ID);
679                },
680
681                addToFastOrder(ELEMENT_ID) {
682                    obFastOrder.get(ELEMENT_ID);
683                },
684
685                plusMinus(data, value) {
686                    return Math.max(data + value, 1);
687                },
688
689                plusMinusMobile(data, value) {
690                    return Math.max(data + value, 0);
691                },
692
693                getCurrencyFormat(price) {
694                    return BX.Currency.currencyFormat(price, 'RUB', true);
695                },
696
697                getCurrencyFormat2(price) {
698                    return BX.Currency.currencyFormat(price, 'RUB', true).replace('₽', '');
699                },
700
701                getPriceByOneVolume(price) {
702                    return Math.ceil(price / this.getVolume);
703                },
704
705                getMinPriceByMetr(price) {
706                    return Math.ceil(this.getPriceByOneVolume(price) / this.Consumption?.TO);
707                },
708
709                setTab(tabName) {
710                    if (tabName === 'review') {
711                        this.initializeAsync();
712                    } else {
713                        // Открыть отзывы можно и по отдельному адресу
714                        // /product/<code>/reviews/ — уходя на другой таб, убираем
715                        // хвост. Обратно его не дописываем: у карточки с SKU
716                        // такого роута нет (urlrewrite знает /reviews/ только без
717                        // sku_), и страница по нему отдавала 404 — адрес ломался
718                        // при перезагрузке и в отправленной кому-то ссылке.
719                        const currentUrl = window.location.href;
720                        const cleanUrl = currentUrl.replace('reviews/', '');
721
722                        if (cleanUrl !== currentUrl) {
723                            window.history.replaceState({}, '', cleanUrl);
724                        }
725                    }
726
727                    this.tabActive = tabName;
728
729                    // Только теперь, после смены tabActive: табы рендерятся через
730                    // v-if, и контейнеры появляются в DOM лишь на этом
731                    // перерисовывании. Раньше $nextTick срабатывал до него.
732                    if (tabName === 'review') {
733                        this.$nextTick(() => this.fillReviewsList());
734                    }
735                    if (tabName === 'tech') {
736                        this.$nextTick(() => this.fillTechSpecs());
737                    }
738                },
739
740                selectImg(currentImg) {
741                    this.getColectionImg.forEach(img => img.active = false);
742                    currentImg.active = true;
743
744                    setTimeout(() => {
745                        if (this.isOboi && !this.isSmallScreen) {
746                            magnify("magnificZoom", 1.7);
747                        }
748                    }, 300);
749                },
750
751                handleLeftArrowClick() {
752                    const images = this.getColectionImg;
753                    const currentIndex = images.findIndex(img => img.active);
754                    const newIndex = currentIndex > 0 ? currentIndex - 1 : images.length - 1;
755
756                    images[currentIndex].active = false;
757                    images[newIndex].active = true;
758                },
759
760                handleRightArrowClick() {
761                    const images = this.getColectionImg;
762                    const currentIndex = images.findIndex(img => img.active);
763                    const newIndex = currentIndex < images.length - 1 ? currentIndex + 1 : 0;
764
765                    images[currentIndex].active = false;
766                    images[newIndex].active = true;
767                },
768
769                handleKeyDown(event) {
770                    if (!this.img.showPopup) return;
771
772                    if (event.key === 'ArrowLeft') {
773                        this.handleLeftArrowClick();
774                    } else if (event.key === 'ArrowRight') {
775                        this.handleRightArrowClick();
776                    } else if (event.key === 'Escape') {
777                        this.img.showPopup = false;
778                    }
779                },
780
781                popupAction(event) {
782                    if (!event.target.closest('.popup-main-content')) {
783                        this.img.showPopup = false;
784                    }
785                },
786
787                findSku(keyProp, prop) {
788                    let skuNoFound;
789                    let newSku;
790
791                    for (let keySku in this.skus) {
792                        let sku = this.skus[keySku];
793                        if (sku.FILTER_PROPS[keyProp]?.VALUE == prop.VALUE) {
794                            skuNoFound = sku;
795                            let goodFilter = true;
796
797                            for (let keyCurrent in this.getCurrentPropsFilter) {
798                                if (keyCurrent != keyProp) {
799                                    if (this.getCurrentPropsFilter[keyCurrent].VALUE != sku.FILTER_PROPS[keyCurrent]?.VALUE) {
800                                        goodFilter = false;
801                                    }
802                                }
803                            }
804
805                            if (goodFilter) {
806                                newSku = sku;
807                                break;
808                            }
809                        }
810                    }
811
812                    this.applyTintPrice();
813
814                    return newSku || skuNoFound;
815                },
816
817                /**
818                 * Строка с ценой колеровки в правой колонке.
819                 *
820                 * Раньше это был побочный эффект findSku(), а findSku зовут привязки
821                 * шаблона (showSkuLink, getPopupImg) — они отрисовываются, только когда
822                 * посетитель доскроллит до соответствующего блока. Из-за этого цвет,
823                 * переданный в адресе (?barcode=…&colorid=…), появл
823ялся не при загрузке,
824                 * а после первой прокрутки. Теперь метод отдельный, и mounted() зовёт его
825                 * сразу.
826                 */
827                applyTintPrice() {
828                    let colordata = jQuery.parseJSON($('.color-box--rect.--gtm-event_opentinter').attr('data-tint-data'));
829                    if (!colordata) {
830                        colordata = jQuery.parseJSON($('.color-box--rect.--gtm-event_opentinter').attr('data-dom-data'));
831                    }
832
833                    var MF_COLOR = $('.MF_COLOR .property-filter-value.__active:first').index();
834                    if (MF_COLOR > 0 && fromColers) {
835                        Object.defineProperty(selectedColor, 1, Object.getOwnPropertyDescriptor(selectedColor, 0));
836                        delete selectedColor[0];
837                        fromColers = false;
838                    }
839
840                    if (selectedColor[MF_COLOR] && colordata?.VOLUME) {
841                        obColorer.showcolor();
842                        selectedColor[MF_COLOR].VOLUME = colordata.VOLUME;
843
844                        var oldprice = parseInt($('.right-sidebar-price-row.--price-box .right-sidebar-price-title.--price-value').text()
845                            .replace(' <span class="rub">₽</span>', '').replace(' ', ''));
846
847                    
848
849                        var collerprice = selectedColorPRICE[MF_COLOR]
850                            ? selectedColorPRICE[MF_COLOR] * selectedColor[MF_COLOR].VOLUME
851                            : selectedColorPRICE1 * selectedColor[MF_COLOR].VOLUME;
852
853                        if (!selectedColorPRICE[MF_COLOR]) {
854                            selectedColorPRICE[MF_COLOR] = selectedColorPRICE1;
855                        }
856
857                        var price = collerprice + oldprice;
858
859                        // Цену за литр и за м² раньше собирали из текста блока «Расход»:
860                        // в нём был отдельный элемент .obiem с объёмом банки («9л»).
861                        // Этот элемент закомментирован (см. include/template_all_vue.php),
862                        // класс obiem переехал на строку «1л = 5-11КВ.М.», и
863                        // Number('1л') стал давать NaN. number_format печатает NaN
864                        // как 0 — обе колонки в строке с колеровкой обнулялись.
865                        // Считаем теми же формулами, что и основная строка цены
866                        // (см. include/price_right_block.php): объём и расход берём
867                        // из данных компонента, а не из разметки.
868                        var priceL = this.getPriceByOneVolume(price);
869                        var priceR = this.getMinPriceByMetr(price);
870
871                        // Колонки показываем ровно там же, где их показывает основная
872                        // строка цены, иначе на товарах без объёма сетка разъедется.
873                        var basePrice = this.currentSku?.new_price_set?.[this.getCurrentPriceType]?.NEW_VALUE;
874                        var showPriceL = this.getPriceByOneVolume(basePrice) != 'Infinity';
875                        var showPriceR = this.getMinPriceByMetr(basePrice) != 'Infinity';
876
877                        $('.right-sidebar-price-row.new-price').remove();
878                        $('.right-sidebar-price-row.--price-box').after(
879                            '<div class="right-sidebar-price-row new-price" data-unit-price="' + price + '">' +
880                            '<div class="right-sidebar-price-title --price-value">' + number_format(price, 0, ".", " ") + ' <span class="rub">₽</span></div>' +
881                            (showPriceL ? '<div class="right-sidebar-price-title">' + number_format(priceL, 0, ".", " ") + ' ₽</div>' : '') +
882                            (showPriceR ? '<div class="right-sidebar-price-title">' + number_format(priceR, 0, ".", " ") + ' ₽</div>' : '') +
883                            '</div>'
884                        );
885                        $('.mk-cartbar-price').text(number_format(price, 0, ".", " ") + ' ₽');
886                    } else {
887                        obColorer.hidecolor();
888                    }
889
890                },
891
892                selectProp(keyProp, prop) {
893                    tobasket = 0;
894                    this.currentSku = this.findSku(keyProp, prop);
895                    this.reloadCalc();
896
897                    const h1Element = document.querySelector('[data-js="h1-sku"]');
898                    if (h1Element) {
899                        h1Element.innerText = this.currentSku?.NAME;
900                    }
901                },
902
903                showSkuLink(keyProp, prop) {
904                    const sku = this.findSku(keyProp, prop);
905                    return window.location.href.replace(/sku_\d+/, 'sku_' + sku.ID);
906                },
907
908                getPopupImg(keyProp, prop) {
909                    const sku = this.findSku(keyProp, prop);
910                    return sku.PREVIEW_PICTURE !== null ? sku.LINK_PREVIEW_PICTURE : this.img?.imgProduct?.srcBig;
911                },
912
913                checkPriceDiscontPrice(oPrice, nPrice) {
914                    return parseFloat(oPrice) > parseFloat(nPrice);
915                },
916
917                checkPropsDisabled(keyProp, prop) {
918                    var poropsfilter = document.getElementsByClassName('property-filter-title');
919                    if (poropsfilter.length == 1) return false;
920
921                    for (let skuKey in this.skus) {
922                        let sku = this.skus[skuKey];
923                        if (sku.FILTER_PROPS[keyProp]?.VALUE == prop.VALUE) {
924                            for (let skuPropKey in sku.FILTER_PROPS) {
925                                if (this.getCurrentPropsFilter[skuPropKey]?.VALUE &&
926                                    this.getCurrentPropsFilter[skuPropKey].VALUE == sku.FILTER_PROPS[skuPropKey].VALUE) {
927                                    return false;
928                                }
929                            }
930                        }
931                    }
932                    return true;
933                },
934
935                filterSkusMarketingText(keyProp, prop) {
936                    if (keyProp !== "UPAKOVKA") return "";
937
938                    let propsFilter = {};
939                    for (let keyPropCurrent in this.getCurrentPropsFilter) {
940                        if (keyPropCurrent != keyProp) {
941                            propsFilter[keyPropCurrent] = this.getCurrentPropsFilter[keyPropCurrent];
942                        }
943                    }
944
945                    for (let keySku in this.skus) {
946                        let sku = this.skus[keySku];
947                        if (prop.VALUE === sku?.FILTER_PROPS[keyProp]?.VALUE) {
948                            let goodFilter = true;
949
950                            for (let keyFilterProp in propsFilter) {
951                                if (propsFilter[keyFilterProp].VALUE !== sku?.FILTER_PROPS[keyFilterProp]?.VALUE) {
952                                    goodFilter = false;
953                                }
954                            }
955
956                            if (goodFilter) {
957                                const obDiscont = this.getDiscontLable(sku);
958                                return obDiscont ? obDiscont.smallLable : false;
959                            }
960                        }
961                    }
962                    return "";
963                },
964
965                getOffsetTop(element) {
966                    let offsetTop = 0;
967                    while (element) {
968                        offsetTop += element.offsetTop;
969                        element = element.offsetParent;
970                    }
971                    return offsetTop;
972                },
973
974                openForm(id) {
975                    obFastOrder.getDolymi(id);
976                    this.toogleModal();
977                },
978
979                toogleModal() {
980                    this.isOpenModal = !this.isOpenModal;
981                },
982
983                setLocalStoregeSkuCurrent(n) {
984                    localStorage.setItem('currentSku', JSON.stringify(n));
985                    window.dispatchEvent(new CustomEvent('currentSku-changed', {
986                        detail: { storage: localStorage.getItem('currentSku') }
987                    }));
988                },
989
990                changeUrlSku(sku) {
991                    const newURL = window.location.href.replace(/sku_\d+/, 'sku_' + sku.ID);
992                    window.history.pushState({ path: newURL }, '', newURL);
993                    $('.buy-button-box .product-cart-control').hide();
994                    $('.buy-button-box .add-to-cart').show();
995
996                },
997
998                getCountBaseSku() {
999                    return this.calcPropertiesFilter?.MF_COLOR?.length;
1000                },
1001
1002                hideCurrentPriceSku() {},
1003
1004                getOtherBase() {
1005                    let propertiesFilter = this.calcPropertiesFilter;
1006                    for (let propKey in propertiesFilter) {
1007                        if (propKey == "MF_COLOR") {
1008                            for (let propValueKey in propertiesFilter[propKey]) {
1009                                if (propertiesFilter[propKey][propValueKey].VALUE !== this.getCurrentPropsFilter[propKey]?.VALUE) {
1010                                    return propertiesFilter[propKey][propValueKey]?.VALUE_PRINT?.VALUE;
1011                                }
1012                            }
1013                        }
1014                    }
1015                },
1016
1017                getBaseNeedColorate() {
1018                    const colors = this.calcPropertiesFilter?.MF_COLOR
1019                        ?.filter(color => this.needColoratesColor?.includes(color.VALUE))
1020                        .map(color => ({ name: color.VALUE_PRINT.VALUE })) || [];
1021
1022                    return `Баз${colors.length > 1 ? 'ы' : 'а'} ${colors.map(c => c.name).join(' ')} ${colors.length > 1 ? 'обязательны' : 'обязательна'} к колеровке`;
1023                },
1024
1025                reloadCalc: function () {
1026                    this.calcOpen = false;
1027                    this.surfaces = [{ width: '', length: '', square: '', square_grout: '', width_seam: '', thickness: '' }];
1028                    this.totalPrice = 0;
1029                    this.totalColorPrice = 0;
1030                    this.totalTovarPrice = 0;
1031                    this.tinsList = [];
1032
1033                    if ($('.title-tint-select').text()) {
1034                        this.RoboTintColor = $('.title-tint-select').text();
1035                        this.RoboTintColorHex = $('.color-box--rect .pic.cl').attr('style');
1036                    } else {
1037                        this.RoboTintColor = false;
1038                        this.RoboTintColorHex = false;
1039                    }
1040
1041                    this.layers.forEach(layer => layer.value = 0);
1042                },
1043
1044                showCalc() {
1045                    this.reloadCalc();
1046                    ym(7500958,'reachGoal','calc');
1047                    $(".product-info-box").after($(".new_block_calc"));
1048                    this.calcOpen = !this.calcOpen;
1049
1050                    if (selectedColor?.[MF_COLOR]) {
1051                        $('.calc-comp__tins-sum-title.note.selectedcolor').html(
1052                            '<span><span class="title-tint-left">Выбран цвет: </span>' +
1053                            '<span class="pic cl" style="background-color:' + selectedColor[MF_COLOR].HEX + '">&nbsp;</span>' +
1054                            '<span class="title-tint">' + $('.title-tint-select').text() + '</span></span>'
1055                        );
1056                    }
1057                    obColorer.showcolor();
1058                },
1059
1060                closeCalc() {
1061                    this.calcOpen = false;
1062                },
1063
1064                handlerSquare: function () {
1065                    this.calculateLayer();
1066                    this.getResult();
1067                },
1068
1069                validateSquare: function (index, type) {
1070                    let currentValue = Math.abs(parseFloat(String(this.surfaces[index][type]).replace(/,/, '.')));
1071                    this.surfaces[index][type] = (typeof currentValue === 'number' && isFinite(currentValue) && currentValue > 0)
1072                        ? currentValue.toFixed(1)
1073                        : '';
1074                },
1075
1076                updateCalcInput: function (index, type, surface) {
1077                    this.validateSquare(index, type);
1078                    this.updateSquare(index, type, surface);
1079
1080                    if (this.surfaces[index]?.length > 0) {
1081                        this.surfaces[index].length = parseFloat(this.surfaces[index].length) + ' м';
1082                    }
1083                    if (this.surfaces[index]?.width > 0) {
1084                        this.surfaces[index].width = parseFloat(this.surfaces[index].width) + ' м';
1085                    }
1086                    if (this.surfaces[index]?.square > 0) {
1087                        this.surfaces[index].square = parseFloat(this.surfaces[index].square) + ' м';
1088                    }
1089                },
1090
1091                updateCalcInputGrout: function (index, type, surface) {
1092                    this.validateSquare(index, type);
1093                    this.updateSquareGrout(index, type, surface);
1094
1095                    if (this.surfaces[index]?.length > 0) {
1096                        this.surfaces[index].length = parseFloat(this.surfaces[index].length);
1097                    }
1098                    if (this.surfaces[index]?.width > 0) {
1099                        this.surfaces[index].width = parseFloat(this.surfaces[index].width);
1100                    }
1101                    if (this.surfaces[index]?.thickness > 0) {
1102                        this.surfaces[index].thickness = parseFloat(this.surfaces[index].thickness);
1103                    }
1104                    if (this.surfaces[index]?.width_seam > 0) {
1105                        this.surfaces[index].width_seam = parseFloat(this.surfaces[index].width_seam);
1106                    }
1107                    if (this.surfaces[index]?.square_grout > 0) {
1108                        this.surfaces[index].square_grout = parseFloat(this.surfaces[index].square_grout);
1109                    }
1110                },
1111
1112                calculateLayer: function () {
1113                    const square = this.calcSquare();
1114                    const paintNeed = parseFloat((square / this.paintRate * 100) / 100).toFixed(2);
1115
1116                    this.layers.forEach(layer => {
1117                        layer.value = layer.val * paintNeed;
1118                    });
1119                },
1120
1121                getCalcLayer: function (layerIndex) {
1122                    let doReload = false;
1123
1124                    this.layers.forEach((layer, index) => {
1125                        layer.active = index === layerIndex;
1126                        if (index !== layerIndex) doReload = true;
1127                    });
1128
1129                    if (doReload) this.getResult();
1130                },
1131
1132                calcSquare: function () {
1133                    return this.surfaces.reduce((total, ele) => {
1134                        if (ele.square) {
1135                            return total + this.clearValue(ele.square);
1136                        }
1137                        return total + this.clearValue(ele.length) * this.clearValue(ele.width);
1138                    }, 0);
1139                },
1140
1141                addSurface() {
1142                    this.surfaces.push({ width: '', length: '', square: '' });
1143                },
1144
1145                clearValue(value) {
1146                    return parseFloat(String(value).replace(/[^\d.,]/g, ''));
1147                },
1148
1149                setValue(index, type) {
1150                    if (this.surfaces[index][type] !== '') {
1151                        this.surfaces[index][type] = this.clearValue(this.surfaces[index][type]);
1152                    }
1153                },
1154
1155                getCalculatedItems: function (volume) {
1156                    let recommendedProducts = this.getFilteredItems();
1157                    let currentVolume = Math.round(volume * 10) / 10;
1158
1159                    recommendedProducts.sort((a, b) => parseFloat(b.VOLUME) - parseFloat(a.VOLUME));
1160
1161                    for (let i = 0; i < recommendedProducts.length; i++) {
1162                        let productVOLUME = parseFloat(recommendedProducts[i].VOLUME);
1163                        recommendedProducts[i].itemsUsed = 0;
1164
1165                        if (currentVolume >= productVOLUME) {
1166                            recommendedProducts[i].itemsUsed = Math.floor(currentVolume / productVOLUME);
1167                            currentVolume = Math.round((currentVolume - (recommendedProducts[i].itemsUsed * productVOLUME)) * 10) / 10;
1168                        }
1169
1170                        if (currentVolume <= 0) break;
1171
1172                        if (i === (recommendedProducts.length - 1) && currentVolume > 0) {
1173                            recommendedProducts[i].itemsUsed += Math.ceil(currentVolume / productVOLUME);
1174                        }
1175                    }
1176
1177                    for (let i = 0; i < recommendedProducts.length; i++) {
1178                        const product = recommendedProducts[i];
1179                        const nextProduct = recommendedProducts[i + 1];
1180
1181                        if (nextProduct && product.itemsUsed > 0 &&
1182                            parseFloat(product.VOLUME) === (parseFloat(nextProduct.VOLUME) * nextProduct.itemsUsed)) {
1183                            recommendedProducts[i].itemsUsed += 1;
1184                            recommendedProducts[i + 1].itemsUsed = 0;
1185                        }
1186                    }
1187
1188                    return recommendedProducts;
1189                },
1190
1191                multiplyVolumeByItemsUsed: function (volume, itemsUsed) {
1192                    return parseFloat(volume) * itemsUsed;
1193                },
1194
1195                getFilteredItems: function () {
1196                    return this.items.filter(item => item.MF_COLOR === this.currentSku?.MF_COLOR);
1197                },
1198
1199                addToBasketCalc: function () {
1200                    if (typeof this.items == "undefined") return;
1201
1202                    const newBasketItems = this.items.filter(e => e.itemsUsed > 0);
1203                    if (!newBasketItems.length) return;
1204
1205                    var MF_COLOR = $('.MF_COLOR .property-filter-value.__active:first').index();
1206                    let colorParams = {};
1207
1208                    if (selectedColor?.[MF_COLOR]) {
1209                        colorParams = {
1210                            COLOR_ID: selectedColor[MF_COLOR].COLOR_ID,
1211                            COLOR_SKU_ID: selectedColor[MF_COLOR].COLOR_SKU_ID,
1212                            COLLECTION_NAME: selectedColor[MF_COLOR].COLLECTION_NAME,
1213                            COLLECTION_ID: selectedColor[MF_COLOR].COLLECTION_ID,
1214                            COLOR_NAME: selectedColor[MF_COLOR].COLOR_NAME,
1215                            HEX: selectedColor[MF_COLOR].HEX,
1216                            COLOR_PRICE: selectedColorPRICE?.[MF_COLOR]
1217                        };
1218                    }
1219
1220                    $.ajax({
1221                        type: "POST",
1222                        url: "/local/components/art-fresh/sale.basket/ajax.php",
1223                        data: {
1224                            items: newBasketItems,
1225                            action: 'calc_basket_add',
1226                            BIG_BASKET: 1,
1227                            basket_new: 'Y',
1228                            sku: this.items.map(item => item.PRODUCT_ID),
1229                            ...colorParams
1230                        },
1231                        timeout: 15000,
1232                        beforeSend: () => {
1233                            $('.calc-comp__accept').remove();
1234                            $(".calc-comp__result-tins.active.is_tins").after('<a href="/personal/cart/" class="calc-comp__accept">Перейти В корзину</a>');
1235
1236                            newBasketItems.forEach(basketItem => {
1237                                this.basketItems.push({
1238                                    ID: basketItem.PRODUCT_ID,
1239                                    QUANTITY: basketItem.itemsUsed
1240                                });
1241                            });
1242                        },
1243                        success: (data) => {
1244                            obBasket.changeBasket(data);
1245
1246                            newBasketItems.forEach(basketItem => {
1247                                $.ajax({
1248                                    url: '/local/ajax/getProduct.php',
1249                                    dataType: 'html',
1250                                    data: { id: basketItem.PRODUCT_ID, quantity: basketItem.itemsUsed },
1251                                    type: 'POST',
1252                                    success: function (data) {
1253                                        var dataJSON = JSON.parse(data);
1254                                        dataLayer.push({
1255                                            "ecommerce": {
1256                                                'currencyCode': 'RUB',
1257                                                "add": {
1258                                                    "products": [{
1259                                                        "id": basketItem.PRODUCT_ID,
1260                                                        "name": dataJSON.name,
1261                                                        "brand": dataJSON.brand,
1262                                                        "category": dataJSON.category,
1263                                                        "price": dataJSON.price,
1264                                                        "variant": dataJSON.variant,
1265                                                        "quantity": basketItem.itemsUsed
1266                                                    }]
1267                                                }
1268                                            },
1269                                            'goods_id': basketItem.PRODUCT_ID,
1270                                            'goods_price': dataJSON.price,
1271                                            'page_type': 'add_product',
1272                                            'event': 'pixel-mg-event',
1273                                            'pixel-mg-event-category': 'Enhanced Ecommerce',
1274                                            'pixel-mg-event-action': 'Adding a Product to a Shopping Cart',
1275                                            'pixel-mg-event-non-interaction': 'False'
1276                                        });
1277                                    },
1278                                    timeout: 2000
1279                                });
1280                            });
1281                        },
1282                    });
1283                },
1284
1285                removeSurface: function (index) {
1286                    this.surfaces.splice(index, 1);
1287                    this.calculateLayer();
1288                    this.getResult();
1289                },
1290
1291                getTinLabel(tin) {
1292                    return `${tin.used} ${PluralForm(tin.used, ['банка', 'банки', 'банок'])} по ${tin.volume}`;
1293                },
1294
1295                getResult: function () {
1296                    const layer = this.layers.find(layer => layer.active);
1297                    if (!layer?.value) return;
1298
1299                    let value = Math.round(layer.value * 10) / 10;
1300                    if (value === 0) value = Math.ceil(layer.value * 10) / 10;
1301
1302                    if (value) {
1303                        var MF_COLOR = $('.MF_COLOR .property-filter-value.__active:first').index();
1304                        let ColorPrice = 0;
1305                        this.totalPrice = 0;
1306                        this.totalColorPrice = 0;
1307                        this.totalTovarPrice = 0;
1308                        this.tinsList = [];
1309
1310                        const tinsList = this.getCalculatedItems(value);
1311                        tinsList.forEach(item => {
1312                            if (item.itemsUsed) {
1313                                this.tinsList.push({
1314                                    used: item.itemsUsed,
1315                                    volume: item.VOLUME
1316                                });
1317                                if (selectedColorPRICE?.[MF_COLOR] > 0) {
1318                                    ColorPrice = Math.round(item.VOLUME) * selectedColorPRICE[MF_COLOR] * item.itemsUsed;
1319                                    this.totalColorPrice += ColorPrice;
1320                                }
1321                                this.totalTovarPrice += item.PRICE * item.itemsUsed;
1322                                this.totalPrice += item.PRICE * item.itemsUsed + ColorPrice;
1323                            }
1324                        });
1325                    }
1326                },
1327
1328                updateSquare(index, type, surface) {
1329                    switch (type) {
1330                        case 'width':
1331                        case 'length':
1332                            if (!surface[type]) {
1333                                surface.square = '';
1334                            } else if (surface.width !== '' && surface.length !== '') {
1335                                surface.square = (this.clearValue(surface.width) * this.clearValue(surface.length)).toFixed(1);
1336                            }
1337                            break;
1338
1339                        case 'square':
1340                            if (!surface[type]) {
1341                                surface.width = '';
1342                                surface.length = '';
1343                            } else if (this.needUpdateSquare(surface)) {
1344                                surface.width = String(this.clearValue(surface[type]));
1345                                surface.length = '1';
1346                            }
1347                            break;
1348                    }
1349
1350                    this.handlerSquare();
1351                },
1352
1353                updateSquareGrout(index, type, surface) {
1354                    this.calculateLayer();
1355                    this.totalWeight = parseFloat(surface.square_grout) *
1356                        (parseFloat(surface.length) + parseFloat(surface.width)) *
1357                        parseFloat(surface.thickness) *
1358                        parseFloat(surface.width_seam) *
1359                        parseFloat(this.density) /
1360                        (parseFloat(surface.length) * parseFloat(surface.width));
1361
1362                    this.totalPrice = 1;
1363                    this.surfaces[index].weight_grout = Math.round(this.totalWeight * 100) / 100 + ' кг';
1364                },
1365
1366                needUpdateSquare(surface) {
1367                    const width = this.clearValue(surface.width);
1368                    const length = this.clearValue(surface.length);
1369                    return (width * length).toFixed(1) !== this.clearValue(surface.square).toFixed(1);
1370                },
1371
1372                getUnitType() {
1373                    if (this.currentSku?.VES_V_KG) return 'кг';
1374                    if (this.currentSku?.OBEM_V_L ||
1375                        this.getPriceByOneVolume(this.currentSku?.new_price_set?.[this.getCurrentPriceType]?.NEW_VALUE) !== 'Infinity') {
1376                        return 'л';
1377                    }
1378                    return this.currentSku?.UNIT_TYPE ? 'л' : 'кг';
1379                },
1380
1381                calcSquareRoom: function () {
1382                    let calcModal = $('.calc-comp');
1383                    let squareTotal = 0;
1384                    let squaresRows = calcModal.find(`[data-block-square=${this.typeRashet}] .calc-comp__square-row`);
1385
1386                    squaresRows.each(function (i, el) {
1387                        let inputLengthValue = $(el).find('.calc-comp__square-input_length').val(),
1388                            inputWidthValue = $(el).find('.calc-comp__square-input_width').val(),
1389                            inputHeightValue = $(el).find('.calc-comp__square-input_height').val(),
1390                            length = parseFloat(inputLengthValue || 0),
1391                            width = parseFloat(inputWidthValue || 0),
1392                            height = parseFloat(inputHeightValue || 0);
1393
1394                        squareTotal += 2 * (length * height) + 2 * (height * width);
1395                    });
1396
1397                    return squareTotal;
1398                },
1399
1400                getResultWallpaper: function () {
1401                    const wallpaperType = this.wallpaperType.find(wallpaper => wallpaper.active);
1402                    if (wallpaperType?.type === 'wall') {
1403                        const square = this.calcSquare();
1404                        const countWallpaper = Math.ceil(square / this.items[0].SQURE_RULON);
1405                        this.items[0].itemsUsed = countWallpaper;
1406                    }
1407                    this.showResultWallpaper();
1408                },
1409
1410                showResultWallpaper: function () {
1411                    this.items.forEach(item => {
1412                        if (item.itemsUsed) {
1413                            this.tinsList.push({
1414                                used: item.itemsUsed,
1415                                volume: item.VOLUME
1416                            });
1417                            this.totalPrice += item.PRICE * item.itemsUsed;
1418                        }
1419                    });
1420                },
1421
1422                buyCheaper: function () {
1423                    const element = document.querySelector('[data-block="line-block-price"]');
1424                    if (element) {
1425                        const topPosition = element.getBoundingClientRect().top + window.scrollY - 150;
1426                        window.scroll({ top: topPosition, behavior: 'smooth' });
1427                        $('.help-text').addClass('active');
1428                    }
1429                },
1430
1431                magnifiedZoom: function () {
1432                    if (this.isOboi && !this.isSmallScreen) {
1433                        magnify("magnificZoom", 1.7);
1434                    }
1435                },
1436
1437                isCashBack() {
1438                    if (!this.currentSku?.MARKETING) return false;
1439
1440                    for (const id in this.currentSku.MARKETING) {
1441                        if (this.currentSku.MARKETING[id].CASH_BACK) return true;
1442                    }
1443                    return false;
1444                },
1445
1446                // Оптимизация: асинхронная инициализация
1447                async initializeAsync() {
1448                    await this.requestReviews();
1449                    this.isDataLoaded = true;
1450                },
1451
1452                // Переносит характеристики из скрытого блока в таб.
1453                // Разметка статичная, без Vue-директив, поэтому innerHTML безопасен.
1454                fillTechSpecs() {
1455                    if (techHtmlCache === null) {
1456                        const source = document.getElementById('techSpecsSource');
1457                        if (!source) {
1458                            return;
1459                        }
1460                        techHtmlCache = source.innerHTML;
1461                        source.remove();
1462                    }
1463
1464                    const mount = document.getElementById('techSpecsMount');
1465                    if (mount && !mount.dataset.loaded) {
1466                        mount.innerHTML = techHtmlCache;
1467                        mount.dataset.loaded = '1';
1468                    }
1469                },
1470
1471                // Переносит список отзывов из скрытого блока в таб — как
1472                // fillTechSpecs. Сервер отдаёт список сразу в HTML (нужен для
1473                // индексации), ajax-запроса больше нет.
1474                // Разметка статичная, без Vue-директив, поэтому innerHTML безопасен.
1475                fillReviewsList() {
1476                    if (reviewsHtmlCache === null) {
1477                        const source = document.getElementById('reviewsListSource');
1478                        if (!source) {
1479                            return;
1480                        }
1481                        reviewsHtmlCache = source.innerHTML;
1482                        source.remove();
1483                    }
1484
1485                    const mount = document.getElementById('reviewsListMount');
1486                    if (mount && !mount.dataset.loaded) {
1487                        mount.innerHTML = reviewsHtmlCache;
1488                        mount.dataset.loaded = '1';
1489                    }
1490                }
1491            },
1492
1493            computed: {
1494                isDiscont() {
1495                    if (this.isUpdating) return false;
1496
1497                    const prices = this.currentSku?.new_price_set;
1498                    if (!prices) return false;
1499
1500                    let checked = false;
1501
1502                    if (this.currentSku?.MARKETING &&
1503                        this.checkPriceDiscontPrice(prices.RRC?.OLD_VALUE, prices.RRC?.NEW_VALUE)) {
1504                        checked = true;
1505                    }
1506
1507                    if (this.checkPriceDiscontPrice(prices.RRC?.OLD_VALUE, prices[this.getCurrentPriceType]?.NEW_VALUE) &&
1508                        this.getCurrentPriceType !== 'RRC' &&
1509                        !this.currentSku?.MARKETING &&
1510                        !checked) {
1511                        checked = true;
1512                    }
1513
1514                    return checked;
1515                },
1516
1517                getSkuBonus() {
1518                    return this.currentSku?.new_price_set?.[this.getCurrentPriceType]?.BONUS_RUB || 0;
1519                },
1520
1521                getReviewAllRating() {
1522                    if (!this.reviews || !Object.keys(this.reviews).length) {
1523                        const initialCount = Number(this.initialReviewCount) || 0;
1524                        if (!initialCount) return false;
1525
1526                        return {
1527                            rating: Number(this.initialReviewRating || 0).toFixed(1),
1528                            count: initialCount,
1529                            countText: pluralForm(initialCount, ["отзыв", "отзыва", "отзывов"])
1530                        };
1531                    }
1532
1533                    let finalRating = 0;
1534                    let finalRatingCount = 0;
1535
1536                    for (let idReview in this.reviews) {
1537                        if (this.reviews[idReview]?.RATING_VALUE > 0) {
1538                            finalRating += Number(this.reviews[idReview].RATING_VALUE);
1539                            finalRatingCount++;
1540                        }
1541                    }
1542
1543                    if (finalRatingCount) {
1544                        const totalReviewCount = Number(this.initialReviewCount) || finalRatingCount;
1545                        const totalReviewRating = Number(this.initialReviewRating) || (finalRating / finalRatingCount);
1546
1547                        return {
1548                            rating: totalReviewRating.toFixed(1),
1549                            count: totalReviewCount,
1550                            countText: pluralForm(totalReviewCount, ["отзыв", "отзыва", "отзывов"])
1551                        };
1552                    }
1553                    return false;
1554                },
1555
1556                getQuantityShop() {
1557                    // доступный остаток меньше 1 считаем нулевым — карточка покажет «Наличие: под заказ»
1558                    const quantity = Number(this.currentSku?.QUANTITY?.SHOP) || 0;
1559                    if (quantity < 1) return 0;
1560                    return quantity > 50 ? "больше 50" : quantity;
1561                },
1562
1563                isRobotint() {
1564                    return this.currentSku?.KOLERUETSYA;
1565                },
1566
1567                isDominanta() {
1568                    let url_string = location.href;
1569                    let url = new URL(url_string);
1570                    let domi = url.searchParams.get("domi");
1571                    return this.brand.NAME == "DOMINANTA" &&
1572                        ["132999", "133000", "132997"].includes(this.currentSku?.MF_COLOR) &&
1573                        this.currentSku?.KOLERUETSYA == 1;
1574                },
1575
1576                // Оптимизация: getCurrentPriceType без побочных эффектов
1577                getCurrentPriceType() {
1578                    if (this.isUpdating) return this.PriceType?.PRICE_CODE || 'RRC';
1579
1580                    const summarRRC = this.calculateSummarRRC();
1581                    this._cachedSummarRRC = summarRRC;
1582
1583                    let resultKeyPrice = "RRC";
1584                    const availablePrices = this.PriceType?.AVAILABLE_PRICES;
1585
1586                    if (availablePrices) {
1587                        for (let keyPrice in availablePrices) {
1588                            if (summarRRC > availablePrices[keyPrice] && keyPrice !== "RRC") {
1589                                resultKeyPrice = keyPrice;
1590                            }
1591                        }
1592                    }
1593
1594                    return resultKeyPrice;
1595                },
1596
1597                getTopSideBar() {
1598                    let marginTop = 220;
1599                    if (this.scroll <= marginTop) return 0;
1600                    return Math.min(this.scroll - marginTop, this.scrollMAX - 650);
1601                },
1602
1603                getConsumption() {
1604                    if (!this.Consumption?.FROM || !this.Consumption?.TO) return false;
1605
1606                    this.Consumption.TYPE = this.getUnitType();
1607                    const digits = this.isOboi ? 0 : 2;
1608
1609                    return {
1610                        all: {
1611                            title: this.Consumption.TYPE == "л" ? "1л" : "1кг",
1612                            value: "= " + this.Consumption.FROM + "-" + this.Consumption?.TO + "КВ.М."
1613                        },
1614                        product: {
1615                            title: (this.Consumption.TYPE == "л" ? this.getVolume + "л" : this.getVolume + "кг"),
1616                            value: "= " + (this.Consumption.FROM * this.getVolume).toFixed(digits) + "-" +
1617                                (this.Consumption?.TO * this.getVolume).toFixed(digits) + "КВ.М."
1618                        }
1619                    };
1620                },
1621
1622                getDensity() {
1623                    return this.density || false;
1624                },
1625
1626                getRoboTintColor() {
1627                    let Color = $('.title-tint-select').text();
1628                    return Color ? 'Выбран цвет: ' + Color : undefined;
1629                },
1630
1631                getCurrentColor() {
1632                    let propertiesFilter = this.propertiesFilter;
1633                    for (let propKey in propertiesFilter) {
1634                        if (propKey == "MF_COLOR") {
1635                            for (let propValueKey in propertiesFilter[propKey]) {
1636                                if (propertiesFilter[propKey][propValueKey].VALUE === this.getCurrentPropsFilter[propKey]?.VALUE) {
1637                                    return propertiesFilter[propKey][propValueKey]?.VALUE_PRINT?.VALUE;
1638                                }
1639                            }
1640                        }
1641                    }
1642                },
1643
1644                getBASE_TYPE_INT() {
1645                    let key = this.currentSku?.BASE;
1646                    return key ? (this.baseRobotint[key]?.COLORATE?.[0] || 0) : 0;
1647                },
1648
1649                getVolume() {
1650                    if (this.currentSku?.OBEM_V_L) {
1651                        return this.currentSku.OBEM_V_L.replace(',', ".");
1652                    }
1653                    return this.currentSku?.VES_V_KG?.replace(',', ".");
1654                },
1655
1656                getTint() {
1657                    let tint = {
1658                        PRODUCT_ID: this.currentSku?.ID,
1659                        PRODUCT_PRICE: this.currentSku?.PRICE_MATRIX?.PRC?.DISCOUNT_VALUE,
1660                        VOLUME: Math.ceil(this.getVolume),
1661                        BASE_TYPE_INT: this.getBASE_TYPE_INT,
1662                        FROM: "Item",
1663                        BARCODE: this.currentSku?.BARCODE,
1664                    };
1665
1666                    let url_string = location.href;
1667                    let url = new URL(url_string);
1668                    let coler = url.searchParams.get("coler");
1669
1670                    if (this.brand.NAME == "DOMINANTA" && this.currentSku?.MF_COLOR == "132999" &&
1671                        this.currentSku?.KOLERUETSYA == 1 && coler == 'Y') {
1672                        tint.BARCODE = '4607026569472';
1673                    }
1674                    if (this.brand.NAME == "DOMINANTA" && this.currentSku?.MF_COLOR == "133000" &&
1675                        this.currentSku?.KOLERUETSYA == 1 && coler == 'Y') {
1676                        tint.BARCODE = '4607026569496';
1677                    }
1678                    return JSON.stringify(tint);
1679                },
1680
1681                getDomi() {
1682                    let BASE_TYPE_INT = this.currentSku?.MF_COLOR == 133000 ? 3 : 1;
1683
1684                    let tint = {
1685                        PRODUCT_ID: this.currentSku?.ID,
1686                        BASE_TYPE_INT,
1687                        COLLECTION_ID: 10000,
1688                        COLLECTION_NAME: "Dominanta",
1689                        COLLECTION_VENDOR_ID: 5,
1690                        COLOR_ID: 127802,
1691                        COLOR_SKU_ID: "163486",
1692                        COLOR_NAME: "DM_С0008",
1693                        COLOR_PRICE: "100",
1694                        BASKET_ID: 0,
1695                        VOLUME: Math.ceil(this.getVolume),
1696                        BASE: this.currentSku?.MF_COLOR,
1697                        BARCODE: this.currentSku?.BARCODE,
1698                    };
1699                    return JSON.stringify(tint);
1700                },
1701
1702                getCurrentImg() {
1703                    return this.getColectionImg?.find(img => img.active === true) || false;
1704                },
1705
1706                // Оптимизация: кэширование getColectionImg
1707                getColectionImg() {
1708                    if (this._cachedCollectionImg && this._cachedSkuId === this.currentSku?.ID) {
1709                        return this._cachedCollectionImg;
1710                    }
1711
1712                    let collectionImg = [];
1713                    let collectionImgDop = this.img?.collectionImgOther ? [...this.img.collectionImgOther] : [];
1714
1715                    if (this.img?.imgProduct?.srcBig) {
1716                        collectionImg.unshift(this.img.imgProduct);
1717                    }
1718
1719                    if (this.currentSku?.ID && this.img?.imgSkus?.[this.currentSku.ID]?.srcBig) {
1720                        collectionImg.splice(0, 1, this.img.imgSkus[this.currentSku.ID]);
1721                    }
1722
1723                    if (this.currentSku?.ID && this.img?.imgSkus?.[this.currentSku.ID]?.MORE_PICTURE) {
1724                        for (const j in this.img.imgSkus[this.currentSku.ID].MORE_PICTURE) {
1725                            collectionImg.push(this.img.imgSkus[this.currentSku.ID].MORE_PICTURE[j]);
1726                        }
1727                    }
1728
1729                    const result = [...collectionImg, ...collectionImgDop];
1730
1731                    this._cachedCollectionImg = result;
1732                    this._cachedSkuId = this.currentSku?.ID;
1733
1734                    return result;
1735                },
1736
1737                getCurrentPropsFilter() {
1738                    if (!this.currentSku?.FILTER_PROPS) return {};
1739
1740                    if (this._cachedCurrentPropsFilter && this._lastSkuId === this.currentSku.ID) {
1741                        return this._cachedCurrentPropsFilter;
1742                    }
1743
1744                    const result = {};
1745                    for (let prop in this.currentSku.FILTER_PROPS) {
1746                        if (prop && this.currentSku.FILTER_PROPS[prop].VALUE) {
1747                            result[prop] = this.currentSku.FILTER_PROPS[prop];
1748                        }
1749                    }
1750
1751                    this._cachedCurrentPropsFilter = result;
1752                    this._lastSkuId = this.currentSku.ID;
1753
1754                    return result;
1755                },
1756
1757                getDolymiPrice: function () {
1758                    let currentQuantity = this.dataSkuJs[this.currentSku?.ID]?.QUANTITY_DEFAULT;
1759                    if (this.isSmallScreen) {
1760                        currentQuantity = this.countItemMobile || 1;
1761                    }
1762
1763                    const totalPrice = (this.currentSku?.new_price_set?.[this.getCurrentPriceType]?.NEW_VALUE || 0) * currentQuantity;
1764                    return Math.ceil(totalPrice / 4);
1765                },
1766
1767                inBasketItems: function () {
1768                    this.currentBasketItem = this.inBasket();
1769                    this.countItemMobile = this.currentBasketItem ? parseInt(this.currentBasketItem.QUANTITY) : 0;
1770                    return this.currentBasketItem;
1771                },
1772
1773                inBasketItemss: function () {
1774                    this.currentBasketItem = this.inBasket();
1775                    return tobasket == 1 ? this.currentBasketItem : false;
1776                },
1777            },
1778
1779            mounted() {
1780                this.slideWidth = this.$refs.slider?.offsetWidth;
1781
1782                // Критическая инициализация
1783                this.iHeight = window.innerHeight / 10;
1784                this.iOffset = window.scrollY || document.documentElement.scrollTop;
1785
1786                // Слушатели событий
1787                window.addEventListener('scroll', this.handleScroll);
1788                window.addEventListener('resize', this.handleResize);
1789                document.addEventListener('keydown', this.handleKeyDown);
1790
1791                this._calcAddToCartHandler = (event) => this.handleCalcAddToCart(event);
1792                window.addEventListener('calc:add-to-cart', this._calcAddToCartHandler);
1793
1794                // Асинхронная инициализация некритических данных
1795                this.$nextTick(() => {
1796                    this.pushEcommerceDetail();
1797                    this.getCurrentDiscontLable();
1798                    this.initSlider();
1799                    this.magnifiedZoom();
1800                    this.getSkuDataPrice();
1801
1802                    // Цвет из адреса должен встать сразу, а не после прокрутки.
1803                    // Ждём ещё один тик: строка цены к этому моменту отрисована,
1804                    // иначе слагаемое oldprice читается как NaN.
1805                    if (window.fromColers) {
1806                        this.$nextTick(() => this.applyTintPrice());
1807                    }
1808
1809                    // Отложенная загрузка отзывов
1810                    if (this.tabActive === 'review') {
1811                        this.initializeAsync();
1812                        this.fillReviewsList();
1813                    }
1814                    if (this.tabActive === 'tech') {
1815                        this.fillTechSpecs();
1816                    }
1817                });
1818            },
1819
1820            unmounted() {
1821                if (this._calcAddToCartHandler) {
1822                    window.removeEventListener('calc:add-to-cart', this._calcAddToCartHandler);
1823                }
1824                window.removeEventListener('scroll', this.handleScroll);
1825                window.removeEventListener('resize', this.handleResize);
1826                document.removeEventListener('keydown', this.handleKeyDown);
1827            },
1828
1829            watch: {
1830                currentSku(newVal, oldVal) {
1831                    if (this.isUpdating || !newVal) return;
1832                    this.isUpdating = true;
1833
1834                    this.setLocalStoregeSkuCurrent(newVal);
1835                    this.changeUrlSku(newVal);
1836
1837                    // Сбрасываем кэши при смене SKU
1838                    this._cachedCollectionImg = null;
1839                    this._cachedSkuId = null;
1840                    this._cachedCurrentPropsFilter = null;
1841
1842                    this.$nextTick(() => {
1843                        this.getCurrentDiscontLable();
1844                        this.getSkuDataPrice();
1845                        this.isUpdating = false;
1846                    });
1847                },
1848
1849                _cachedSummarRRC(newVal) {
1850                    if (newVal) {
1851                        this.$nextTick(() => {
1852                            obDetailProduct.setSlider(parseFloat(newVal));
1853                        });
1854                    }
1855                }
1856            },
1857};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.