Сейчас узнаем, какой подарок выпадет именно Вам
Крутите колесо
Как создать шкалу оптовых цен для товара в Tilda

Как создать шкалу оптовых цен для товара в Tilda

1
Создали каталог с товарами и вывели товары в товарный блок (в примере это ST305N)
2
Товарам с оптовой сеткой прописали дополнительную опцию как на скрине
3
Название этой опции должно совпадать с этим названием в коде
sizeLabel = 'Ценовой диапазон';
4
Вставили код на страницу в блок Т123
В общий и продуктовый footer
Mo-ti Level Up
Видео инструкции по добавлению кода и работе с Zero Block.
Как создать шкалу оптовых цен для товара в Tilda
Фрагмент видео
Библиотека для примера
<script>
document.addEventListener("DOMContentLoaded", function() {
(function () {
  
  const sizeLabel = 'Ценовой диапазон';
  // true = на все товары, false = только на товары с sizeLabel
  const allProducts = false; 

  //шаг увеличения
  const STEP_FOR_ONE = 10; // для товаров от 1шт
  const STEP_FOR_MANY = 50; // для всех остальных товаров

  const wholesaleTitle = "Диапазон цен: "; //Заголовок опции в в карточке товара
  const wholesale = true;  
  
  function hasWholesaleOption(product) {
    if (!product) return false;
    return Array.from(product.querySelectorAll('.js-product-option-name'))
      .some(el => el.textContent.trim() === sizeLabel);
  }

  function getFirstNumber(str) {
    if (!str) return 0;
    const m = String(str).match(/\d+/);
    return m ? parseInt(m[0], 10) : 0;
  }


  function cacheQty(product, val) {
    if (!product) return;
    product.dataset._qty = val;
  }

  function setupWholesaleUI(product) {
    if (!product || product.querySelector('.wholesale-wrapper')) return;
    const optNames = product.querySelectorAll('.js-product-option-name');
    const target = Array.from(optNames).find(el => el.textContent.trim() === sizeLabel);
    if (!target) return;
    const wrap = target.closest('.js-product-option');
    if (!wrap) return;
    wrap.classList.add('wholesale', 't-text');
    const sel = wrap.querySelector('select');
    if (sel) sel.disabled = true;
    if (!wholesale) return;
    
    let curr = '';
    const currEl = product.querySelector('div[class*="_price-currency"]');
    if (currEl && currEl.textContent.trim()) curr = currEl.textContent.trim();
    
    if (sel && sel.options.length > 0) {
      let html = '';
      Array.from(sel.options).forEach(opt => {
        const txt = opt.textContent.trim();
        const pr = opt.getAttribute('data-product-variant-price') || '';
        if (txt && pr) {
          html += `<li data-threshold="${getFirstNumber(opt.value)}">${txt}</li>`;
        }
      });
      wrap.insertAdjacentHTML('afterbegin', `
        <div class="wholesale-wrapper">
          <div class="wholesale-title">${wholesaleTitle}</div>
          <ul class="wholesale-option">${html}</ul>
        </div>`);
    }
  }


  function getStepAndMin(product) {
    const optNames = product.querySelectorAll('.js-product-option-name');
    const target = Array.from(optNames).find(el => el.textContent.trim() === sizeLabel);
    let min = 1;
    if (target) {
      const sel = target.closest('.js-product-option')?.querySelector('select');
      if (sel?.options?.length) min = getFirstNumber(sel.options[0].value) || 1;
    }
    return { step: min === 1 ? STEP_FOR_ONE : STEP_FOR_MANY, min };
  }


  function selectBestOption(product, qty) {
    const target = Array.from(product.querySelectorAll('.js-product-option-name'))
      .find(el => el.textContent.trim() === sizeLabel);
    if (!target) return;
    const sel = target.closest('.js-product-option')?.querySelector('select');
    if (!sel?.options.length) return;
    
    const opts = Array.from(sel.options).map(o => ({el: o, v: getFirstNumber(o.value)})).sort((a,b)=>a.v-b.v);
    let best = opts[0];
    for (const o of opts) { if (o.v <= qty) best = o; else break; }
    
    if (sel.value !== best.el.value) {
      sel.value = best.el.value;
      sel.dispatchEvent(new Event('change', { bubbles: true }));
      setTimeout(() => { createTotalWrapper(product); updateActiveRule(product); }, 200);
    } else {
      updateActiveRule(product);
    }
  }


  function updateActiveRule(product) {
    const target = Array.from(product.querySelectorAll('.js-product-option-name'))
      .find(el => el.textContent.trim() === sizeLabel);
    if (!target) return;
    const sel = target.closest('.js-product-option')?.querySelector('select');
    if (!sel) return;
    const cur = getFirstNumber(sel.value);
    const w = product.querySelector('.wholesale-wrapper');
    if (!w) return;
    w.querySelectorAll('.wholesale-option li').forEach(li => {
      li.classList.toggle('active-rule', parseInt(li.dataset.threshold) === cur);
    });
  }

 
  function parsePrice(t) {
    if (!t) return NaN;
    return parseFloat(t.replace(/[^\d.,]/g, '').replace(',', '.'));
  }


  function createTotalWrapper(p) {
    const inp = p.querySelector('.t-store__prod__quantity-input');
    if (!inp) return;
    const q = parseInt(inp.value);
    if (isNaN(q) || q <= 0) return;
    const pw = p.querySelector('.js-store-price-wrapper');
    if (!pw) return;
    const pe = pw.querySelector('.js-product-price');
    if (!pe) return;
    const pr = parsePrice(pe.textContent);
    if (isNaN(pr) || pr <= 0) return;
    let c = '';
    const ce = p.querySelector('div[class*="_price-currency"]');
    if (ce) c = ce.textContent.trim();
    const tot = Math.round(pr * q * 100) / 100;
    pw.setAttribute('data-total-label', `${pr} x ${q} = ${tot} ${c}`.trim());
    pw.classList.add('t-text');
    window.dispatchEvent(new Event('resize', { bubbles: true }));
  }


  function handleBlur(e) {
    const inp = e.target;
    const p = inp.closest('.js-product');
    if (!p) return;
    const { min } = getStepAndMin(p);
    let v = parseInt(inp.value) || min;
    if (v < min) v = min;
    inp.value = v; cacheQty(p, v);
    inp.dispatchEvent(new Event('change', { bubbles: true }));
    createTotalWrapper(p); selectBestOption(p, v); updateActiveRule(p);
  }


  function initProduct(p) {
    if (!p || p.dataset.qtyLocked) return;
    const inp = p.querySelector('.t-store__prod__quantity-input');
    const pe = p.querySelector('.js-product-price');
    if (!inp || !pe) return;
    p.dataset.qtyLocked = "true";
    const { min, step } = getStepAndMin(p);
    
    inp.value = min; cacheQty(p, min);
    inp.dispatchEvent(new Event('input', { bubbles: true }));
    inp.dispatchEvent(new Event('change', { bubbles: true }));
    
    setTimeout(() => { createTotalWrapper(p); selectBestOption(p, min); updateActiveRule(p); }, 150);
    setTimeout(() => {
      if (parseInt(inp.value) < min) {
        inp.value = min; cacheQty(p, min);
        inp.dispatchEvent(new Event('change', { bubbles: true }));
        createTotalWrapper(p); updateActiveRule(p);
      }
    }, 350);
    
    if (!inp._blurAttached) {
      inp.addEventListener('blur', handleBlur);
      inp._blurAttached = true;
    }
    setupWholesaleUI(p);
  }
  

  function runInit() {
    document.querySelectorAll('.js-product:not([data-qty-locked])').forEach(p => {
      if (allProducts || hasWholesaleOption(p)) {
        p.classList.add('wholesale-product'); 
        initProduct(p);
      }
    });
  }
  runInit();
  
  document.querySelectorAll('.js-store-grid-cont, .js-product-single, .t-store__relevants-grid-cont').forEach(el => {
    ['tStoreRendered', 'tStoreSingleProductsLoaded'].forEach(e => el.addEventListener(e, () => setTimeout(runInit, 150)));
  });
  document.addEventListener('click', e => { if(e.target.closest('a[href*="/tproduct/"], a[href^="#order"]')) setTimeout(runInit, 300); });
  document.addEventListener('change', e => { if(e.target.closest('.js-product-controls-wrapper')) setTimeout(runInit, 150); });
  
  document.addEventListener('input', e => {
    if (e.target.matches('.t-store__prod__quantity-input')) {
      const p = e.target.closest('.js-product');
      if (p) cacheQty(p, e.target.value);
    }
  });

  document.addEventListener('click', function(ev) {
    const plus = ev.target.closest('.t-store__prod__quantity__plus-wrapper');
    if (plus) {
      const p = plus.closest('.js-product');
      if (!allProducts && !hasWholesaleOption(p)) return;
      ev.stopImmediatePropagation(); ev.preventDefault();
      const { step, min } = getStepAndMin(p);
      const inp = plus.closest('.t-store__prod__quantity')?.querySelector('.t-store__prod__quantity-input');
      if (inp) {
        let v = parseInt(inp.value) || min;
        v = (min === 1 && v === 1) ? STEP_FOR_ONE : v + step;
        inp.value = v; cacheQty(p, v);
        inp.dispatchEvent(new Event('change', { bubbles: true }));
        createTotalWrapper(p); selectBestOption(p, v); updateActiveRule(p);
      }
      return;
    }
    const minus = ev.target.closest('.t-store__prod__quantity__minus-wrapper');
    if (minus) {
      const p = minus.closest('.js-product');
      if (!allProducts && !hasWholesaleOption(p)) return;
      ev.stopImmediatePropagation(); ev.preventDefault();
      const { step, min } = getStepAndMin(p);
      const inp = minus.closest('.t-store__prod__quantity')?.querySelector('.t-store__prod__quantity-input');
      if (inp) {
        let v = parseInt(inp.value) || min;
        v = Math.max(min, v - step);
        inp.value = v; cacheQty(p, v);
        inp.dispatchEvent(new Event('change', { bubbles: true }));
        createTotalWrapper(p); selectBestOption(p, v); updateActiveRule(p);
      }
    }
  }, true);

  let _cartIdx = null;
  document.addEventListener('focusin', e => {
    const inp = e.target.closest('.t706__product-quantity-inp');
    if (inp) _cartIdx = inp.closest('.t706__product')?.dataset.cartProductI;
  });


  if (typeof t_onReady === 'function') {
    t_onReady(() => setTimeout(() => { if (typeof t_onFuncLoad === 'function') t_onFuncLoad('tcart__init', changeCart); }, 200));
  }
  
  function changeCart(){
    function getCartStep(prod) {
      if (!prod.wholesale_option) return 1;
      const t = Object.keys(prod.wholesale_option).map(Number);
      if (!t.length) return 1;
      return Math.min(...t) === 1 ? STEP_FOR_ONE : STEP_FOR_MANY;
    }

    if (typeof tcart__product__plus === 'function') {
      const orig = tcart__product__plus;
      tcart__product__plus = function(btn) {
        const el = btn.closest(".t706__product");
        const i = el?.dataset.cartProductI;
        if (!i || !window.tcart?.products?.[i]) return orig(btn);
        const prod = window.tcart.products[i];
        const step = getCartStep(prod);
        const minT = prod.wholesale_option ? Math.min(...Object.keys(prod.wholesale_option).map(Number)) : 1;
        if (prod.inv > 0 && prod.inv === prod.quantity) {
          if (typeof tcart_dict === 'function') alert(tcart_dict("limitReached"));
          return;
        }
        if (prod.wholesale_option && minT === 1 && prod.quantity === 1) prod.quantity = STEP_FOR_ONE;
        else prod.quantity += step;
        prod.amount = typeof tcart__roundPrice === 'function' ? tcart__roundPrice(prod.price * prod.quantity) : Math.round(prod.price * prod.quantity * 100) / 100;
        const q = el.querySelector(".t706__product-quantity"); if(q) q.innerHTML = prod.quantity;
        if(prod.single === "y" && prod.portion && typeof tcart__showWeight === 'function'){ const p = el.querySelector(".t706__product-portion"); if(p) p.innerHTML = tcart__showWeight(prod.quantity * prod.portion, prod.unit); }
        if(prod.amount > 0 && typeof tcart__showPrice === 'function'){ const a = el.querySelector(".t706__product-amount"); if(a) a.innerHTML = tcart__showPrice(prod.amount); }
        if(typeof tcart__updateTotalProductsinCartObj==='function') tcart__updateTotalProductsinCartObj();
        if(typeof tcart__reDrawCartIcon==='function') tcart__reDrawCartIcon();
        if(typeof tcart__reDrawTotal==='function') tcart__reDrawTotal();
        if(typeof tcart__saveLocalObj==='function') tcart__saveLocalObj();
      };
    }
    if (typeof tcart__product__minus === 'function') {
      const orig = tcart__product__minus;
      tcart__product__minus = function(btn) {
        const el = btn.closest(".t706__product");
        const i = el?.dataset.cartProductI;
        if (!i || !window.tcart?.products?.[i]) return orig(btn);
        const prod = window.tcart.products[i];
        const step = getCartStep(prod);
        const minT = prod.wholesale_option ? Math.min(...Object.keys(prod.wholesale_option).map(Number)) : 1;
        prod.quantity = prod.wholesale_option ? Math.max(minT, prod.quantity - step) : Math.max(1, prod.quantity - step);
        prod.amount = typeof tcart__roundPrice === 'function' ? tcart__roundPrice(prod.price * prod.quantity) : Math.round(prod.price * prod.quantity * 100) / 100;
        if(prod.amount > 0 && typeof tcart__showPrice === 'function'){ const a = el.querySelector(".t706__product-amount"); if(a) a.innerHTML = tcart__showPrice(prod.amount); if(prod.single === "y" && prod.portion && typeof tcart__showWeight === 'function'){ const p = el.querySelector(".t706__product-portion"); if(p) p.innerHTML = tcart__showWeight(prod.quantity * prod.portion, prod.unit); } }
        const q = el.querySelector(".t706__product-quantity"); if(q) q.innerHTML = prod.quantity;
        if(prod.quantity <= 0 && typeof tcart__product__del === 'function') tcart__product__del(btn);
        if(typeof tcart__updateTotalProductsinCartObj==='function') tcart__updateTotalProductsinCartObj();
        if(typeof tcart__reDrawCartIcon==='function') tcart__reDrawCartIcon();
        if(typeof tcart__reDrawTotal==='function') tcart__reDrawTotal();
        if(typeof tcart__saveLocalObj==='function') tcart__saveLocalObj();
      };
    }
  }
  
  document.addEventListener('click', function(e) {
    const link = e.target.closest('a[href^="#order"]');
    if (!link) return;
    const p = link.closest('.js-product');
    if (!p) return;
    const wb = p.querySelector('.wholesale');
    if (!wb) return; 
    const uid = p.dataset.productGenUid;
    if (!uid) return;
    const sel = wb.querySelector('select');
    if (!sel?.options.length) return;

    const savedQty = parseInt(p.dataset._qty, 10) || 1;

    const wOpt = {};
    Array.from(sel.options).forEach(opt => {
      const n = getFirstNumber(opt.value);
      if (!n) return;
      const pr = opt.getAttribute('data-product-variant-price');
      if (!pr) return;
      wOpt[n] = { option: sizeLabel, variant: opt.textContent.trim(), price: parseFloat(pr) };
    });

    setTimeout(() => {
      if (!window.tcart?.products) return;
      let upd = false;
      for (const id in window.tcart.products) {
        const prod = window.tcart.products[id];
        if (prod.gen_uid === uid) {
          prod.wholesale_option = wOpt;
          upd = true;
          checkWholesalePrice(id);
        }
      }
      if (upd && typeof tcart__saveLocalObj === 'function') tcart__saveLocalObj();

      if (savedQty > 1) {
        setTimeout(() => {
          const inp = p.querySelector('.t-store__prod__quantity-input');
          if (inp && !inp.disabled) {
            inp.value = savedQty; cacheQty(p, savedQty);
            inp.dispatchEvent(new Event('input', { bubbles: true }));
            inp.dispatchEvent(new Event('change', { bubbles: true }));
            createTotalWrapper(p); selectBestOption(p, savedQty); updateActiveRule(p);
          }
        }, 200);
      }
    }, 300);
  }, false);

  function checkWholesalePrice(i) {
    if (!window.tcart?.products?.[i]) return;
    const prod = window.tcart.products[i];
    if (!prod.wholesale_option) return;
    const qty = prod.quantity;
    const w = prod.wholesale_option;
    const th = Object.keys(w).map(Number).sort((a,b)=>a-b);
    let best = th[0];
    for (const t of th) { if (t <= qty) best = t; else break; }
    const b = w[best];
    if (!b) return;
    const idx = prod.options?.findIndex(o => o.option === sizeLabel);
    if (idx === -1 || idx === undefined) return;
    const cur = prod.options[idx];
    if (cur.variant !== b.variant || cur.price !== b.price) {
      prod.options[idx].variant = b.variant;
      prod.options[idx].price = b.price;
      prod.price = b.price;
      prod.amount = typeof tcart__roundPrice === 'function' ? tcart__roundPrice(b.price * qty) : Math.round(b.price * qty * 100) / 100;
      if (typeof tcart__reDrawProducts === 'function') tcart__reDrawProducts();
      if (typeof tcart__updateTotalProductsinCartObj === 'function') tcart__updateTotalProductsinCartObj();
      if (typeof tcart__reDrawTotal === 'function') tcart__reDrawTotal();
      if (typeof tcart__saveLocalObj === 'function') tcart__saveLocalObj();
    }
  }

  document.addEventListener('click', e => {
    if (e.target.closest('.t706__product-plus, .t706__product-minus')) {
      const p = e.target.closest('.t706__product');
      if (p?.dataset.cartProductI) setTimeout(() => checkWholesalePrice(p.dataset.cartProductI), 100);
    }
  });
  document.addEventListener('focusout', e => {
    if (e.target.closest('.t706__product-quantity-inp') && _cartIdx != null) setTimeout(() => checkWholesalePrice(_cartIdx), 100);
  });
  document.addEventListener('change', e => {
    if (e.target.closest('.t706__product-quantity-inp') && _cartIdx != null) setTimeout(() => checkWholesalePrice(_cartIdx), 150);
  });

})();
});
</script>

<style>
.t706__cartwin-content {
    max-width: 800px;
}
.wholesale-option li {
    transition: all 0.2s ease;
    padding: 4px 8px;
    border-radius: 4px;
    list-style: none;
}
.wholesale-option li.active-rule {
    background: #f0f0f0;
    font-weight: 600;
    color: #000;
}
.js-product.wholesale-product .js-store-price-wrapper:after {
    content: "Всего: " attr(data-total-label);
    display: block;
    font-size: 14px;
}
.wholesale-wrapper {
    margin-bottom: 15px;
    padding: 10px;
    background: #f8f9fa;
    border-radius: 6px;
    font-size: 14px;
}
.wholesale-title {
    font-weight: 600;
    margin-bottom: 8px;
}
#allrecords .wholesale-option {
    list-style: none;
    padding: 0;
    margin: 0;
}
.wholesale-option li {
    padding: 4px 0;
    border-bottom: 1px dashed #ddd;
    font-size: 12px;
}
.wholesale-option li:last-child {
    border-bottom: none;
}
.js-product-option.wholesale select[disabled] {
    pointer-events: none;
    opacity: 0.5;
}
.wholesale .js-product-option-name,
.wholesale .t-product__option-variants
{
    display: none;
}
.js-product-controls-wrapper.t-store__card__prod-controls-wrapper {
    padding-top: 10px;
}
</style>
Made on
Tilda