В этой статье мы разберём, как создать виджет для платформы InSales, который отображает интерактивную карту с филиалами или магазинами, используя Яндекс.Карты. Виджет будет включать табы с городами слева и карту справа, с возможностью фокусировки на всех точках выбранного города или конкретного магазина. Мы основываемся на реальной разработке, включающей настройку шаблонов, JavaScript и стилей, чтобы обеспечить гибкость и удобство для пользователей.


1. Создание блока с магазинами в шаблонах

Сначала создаём блок данных для хранения информации о магазинах. Это делается в админ-панели InSales в разделе Темы → Настройки → Блоки.

Настройки блока:

  • Название: Магазины
  • Поля:
    • Название (name, текст) — название магазина, например, «Магазин 1».
    • Адрес (address, текст) — адрес без указания города, например, «ул. Примерная, 1».
    • Координаты (geo, текст) — координаты в формате широта,долгота, например, 55.792806,36.905985.
    • Телефон (phone, текст) — номер телефона, например, +79991234567.
    • Город (regions, списочное) — город, например, Москва (список, чтобы поддерживать несколько значений, но в нашем случае используется одно).

Пример данных блока:

[
  {
    "name": "Магазин 1",
    "address": "ул. Примерная, 1",
    "geo": "55.792806,36.905985",
    "phone": "+79991234567",
    "regions": ["Москва"]
  },
  {
    "name": "Магазин 2",
    "address": "ул. Невская, 3",
    "geo": "59.9343,30.3351",
    "phone": "8 (812) 123-45-67",
    "regions": ["Санкт-Петербург"]
  }
]

2. Создание виджета с блоком магазинов

В админ-панели InSales переходим в Темы → Виджеты и создаём новый виджет:

  • Название: Контакты с картой
  • Тип: Блочный
  • Блок: Магазины (выбираем созданный блок).
  • Дополнительные настройки (опционально):
    • banner_name (текст) — заголовок виджета, например, «Наши магазины».
    • phone_link (текст) — общий телефон, например, +78001234567.
    • mail_link (текст) — общий email, например, info@shop.ru.

После создания виджет будет доступен для добавления на страницы.

3. Создание шаблона для вывода с картой и табами

Создаём файл шаблона виджета в Темы → Редактировать код → Templates → contacts.liquid. Шаблон отображает табы с городами слева и Яндекс.Карту справа. При клике на город карта показывает все точки этого города, при клике на магазин — фокусируется на нём.

<div class="contacts-container">
    <div class="contacts">
      <div class="contacts-title heading">
        {{ widget_settings.banner_name | editable }}
      </div>
      <div class="contacts-title mobile heading">
        {{ widget_settings.banner_name | editable }}
      </div>
      <div class="contacts-content">
        <!-- Левая часть: Табы с городами и магазинами -->
        <div class="contacts-tabs">
          <ul class="tabs__nav">
            {% assign regions = data.blocks | map: 'regions' | uniq %}
            {% for region in regions %}
              <li class="tabs__nav-item {% if forloop.first %}active{% endif %}" data-tab="{{ widget.id }}-region-{{ forloop.index0 }}">{{ region | editable }}</li>
            {% endfor %}
          </ul>
          <div class="tabs__content">
            {% for region in regions %}
              <div class="tabs__content-item {% if forloop.first %}active{% endif %}" id="tab-{{ widget.id }}-region-{{ forloop.index0 }}">
                <ul class="shops__list">
                  {% for block in data.blocks %}
                    {% if block.regions contains region %}
                      <li class="shops__item {% if forloop.first %}active{% endif %}" data-geo="{{ block.geo | strip }}" data-shop-id="{{ widget.id }}-shop-{{ forloop.index0 }}">
                        <div class="shops__item-name">{{ block.name | editable }}</div>
                        <div class="shops__item-address">г. {{ block.regions | first | editable }}, {{ block.address | editable }}</div>
                        {% if block.work != blank %}
                          <div class="shops__item-work">{{ block.work | editable }}</div>
                        {% endif %}
                        {% if block.phone != blank %}
                          <div class="shops__item-phone">
                            <a href="tel:{{ block.phone | strip_html | remove: '(' | remove: ')' | remove: '-' | remove: ' ' | lstrip }}">{{ block.phone | editable }}</a>
                          </div>
                        {% endif %}
                      </li>
                    {% endif %}
                  {% endfor %}
                </ul>
              </div>
            {% endfor %}
          </div>
          <!-- Контакты -->
          <div class="contacts-blocks__links">
            {% if widget_settings.phone_link != blank %}
            <div class="contacts-blocks__links-phone">
              <a href="tel:{{ widget_settings.phone_link | strip_html | remove: '(' | remove: ')' | remove: '-' | remove: ' ' | lstrip }}">{{ widget_settings.phone_link }}</a>
            </div>
            {% endif %}
            {% if widget_settings.mail_link != blank %}
            <div class="contacts-blocks__links-mail">
              <a href="mailto:{{ widget_settings.mail_link }}">{{ widget_settings.mail_link }}</a>
            </div>
            {% endif %}
          </div>
        </div>
        <!-- Правая часть: Карта -->
        <div class="contacts-map">
          <div id="yandex-map-{{ widget.id }}" style="width: 100%; height: 400px;"></div>
          <!-- Передаём координаты и дополнительные данные в JSON -->
          <script type="application/json" id="map-coordinates-{{ widget.id }}">
            [
              {% for block in data.blocks %}
                {% if block.geo != blank %}
                  {
                    "name": "{{ block.name | escape }}",
                    "address": "{{ block.address | escape }}",
                    "geo": "{{ block.geo | strip }}",
                    "shopId": "{{ widget.id }}-shop-{{ forloop.index0 }}",
                    "region": "{{ block.regions | first | escape }}",
                    "work": "{{ block.work | escape }}",
                    "phone": "{{ block.phone | escape }}"
                  }{% unless forloop.last %},{% endunless %}
                {% endif %}
              {% endfor %}
            ]
          </script>
        </div>
      </div>
    </div>
</div>

<style>
  .contacts-content {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
  }

  .contacts-tabs {
    flex: 1;
    min-width: 300px;
  }

  .contacts-map {
    flex: 1;
    min-width: 300px;
    height: 400px;
  }

  #yandex-map-{{ widget.id }} {
    width: 100%;
    height: 100%;
  }

  .tabs__nav {
    display: flex;
    list-style: none;
    padding: 0;
    margin: 0 0 10px 0;
    border-bottom: 2px solid #ddd;
    flex-wrap: wrap;
  }

  .tabs__nav-item {
    padding: 10px 20px;
    cursor: pointer;
    border-bottom: 2px solid transparent;
    transition: all 0.3s ease;
    font-size: 16px;
    color: #333;
  }

  .tabs__nav-item:hover {
    background-color: #f5f5f5;
    border-bottom-color: #ccc;
  }

  .tabs__nav-item.active {
    border-bottom-color: #007bff;
    color: #007bff;
    font-weight: bold;
  }

  .tabs__content-item {
    display: none;
  }

  .tabs__content-item.active {
    display: block;
    background-color: #ffffff;
  }

  .shops__list {
    list-style: none;
    padding: 0;
    margin: 0;
  }

  .shops__item {
    padding: 15px;
    cursor: pointer;
    border-bottom: 1px solid #eee;
    transition: background-color 0.2s ease;
  }

  .shops__item:hover {
    background-color: #f9f9f9;
  }

  .shops__item.active {
    background-color: #e6f0ff;
    font-weight: bold;
  }

  .shops__item-name {
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 5px;
  }

  .shops__item-address,
  .shops__item-work,
  .shops__item-phone {
    font-size: 14px;
    margin-bottom: 5px;
    color: #555;
  }

  .shops__item-phone a {
    color: #000000;
    text-decoration: none;
  }

  .shops__item-phone a:hover {
    text-decoration: underline;
  }

  .contacts-blocks__links {
    margin-top: 20px;
  }

  .contacts-blocks__links-phone,
  .contacts-blocks__links-mail {
    margin-bottom: 10px;
  }

  @media (max-width: 768px) {
    .contacts-content {
      flex-direction: column;
    }

    .contacts-tabs,
    .contacts-map {
      min-width: 100%;
    }
  }
</style>

Ключевые особенности шаблона:

  • Табы: Города (block.regions) отображаются как вкладки, получаемые через | uniq для избежания дублирования.
  • Магазины: Каждый магазин отображает название, адрес (в формате г. Москва, ул. Примерная, 1), график работы (если есть) и телефон (чёрная кликабельная ссылка).
  • Карта: Контейнер для Яндекс.Карт с уникальным ID (yandex-map-{{ widget.id }}).
  • JSON: Передаёт данные магазинов (name, address, geo, region, work, phone) для JavaScript.

4. Подключение необходимых скриптов

Создаём JavaScript-файл для инициализации карты и работы табов. Файл размещаем в Темы → Редактировать код → Assets → yandex-map-init.js.

(function() {
  let maps = {};

  function initYandexMap(widgetId) {
    console.log('Инициализация карты для виджета: ' + widgetId);

    if (typeof ymaps === 'undefined') {
      console.error('Yandex Maps API не загружен');
      return;
    }

    ymaps.ready(function() {
      const mapId = 'yandex-map-' + widgetId;
      const coordinatesDataId = 'map-coordinates-' + widgetId;
      const mapElement = document.getElementById(mapId);
      const coordinatesDataElement = document.getElementById(coordinatesDataId);

      if (!mapElement) {
        console.error('Контейнер карты #' + mapId + ' не найден');
        return;
      }

      if (!coordinatesDataElement) {
        console.error('Контейнер координат #' + coordinatesDataId + ' не найден');
        return;
      }

      let coordinatesData;
      try {
        coordinatesData = JSON.parse(coordinatesDataElement.textContent);
        console.log('Координаты для виджета ' + widgetId + ':', coordinatesData);
      } catch (e) {
        console.error('Ошибка парсинга JSON для виджета ' + widgetId + ':', e);
        return;
      }

      if (!coordinatesData || coordinatesData.length === 0) {
        console.warn('Нет координат для виджета ' + widgetId);
        mapElement.innerHTML = '<p>Нет данных для отображения карты</p>';
        return;
      }

      // Создаём карту
      const myMap = new ymaps.Map(mapId, {
        center: coordinatesData[0].geo.split(',').map(Number),
        zoom: 10,
        controls: ['zoomControl', 'typeSelector']
      }, {
        suppressMapOpenBlock: true
      });
      maps[widgetId] = myMap;

      // Добавляем метки
      coordinatesData.forEach((block, index) => {
        if (block.geo && block.shopId) {
          const coords = block.geo.split(',').map(Number);
          if (coords.length === 2 && !isNaN(coords[0]) && !isNaN(coords[1])) {
            const placemark = new ymaps.Placemark(
              [coords[0], coords[1]],
              {
                balloonContentHeader: block.name || 'Без названия',
                balloonContentBody: `
                  <div>${block.region || 'Город не указан'}</div>
                  <div>${block.address || 'Адрес не указан'}</div>
                  ${block.work ? `<div>График: ${block.work}</div>` : ''}
                  ${block.phone ? `<div>Телефон: <a href="tel:${block.phone.replace(/[\(\)\-\s]/g, '')}">${block.phone}</a></div>` : ''}
                `,
                hintContent: block.name || 'Точка ' + (index + 1)
              },
              {
                preset: 'islands#blackCircleDotIcon',
                hideIconOnBalloonOpen: false
              }
            );
            placemark.properties.set('shopId', block.shopId);
            placemark.properties.set('region', block.region);
            myMap.geoObjects.add(placemark);
          } else {
            console.warn('Некорректный формат координат в блоке ' + index + ':', block.geo);
          }
        }
      });

      // Устанавливаем границы карты для всех точек
      const bounds = myMap.geoObjects.getBounds();
      if (bounds) {
        myMap.setBounds(bounds, { checkZoomRange: true, duration: 300 });
      }

      // Обработчик кликов по магазинам
      document.querySelectorAll(`#tab-${widgetId}-region-0 .shops__item`).forEach(item => item.classList.add('active'));
      document.querySelectorAll(`[id^="tab-${widgetId}-region-"] .shops__item`).forEach(item => {
        item.addEventListener('click', function() {
          const geo = this.getAttribute('data-geo');
          const shopId = this.getAttribute('data-shop-id');
          if (geo && shopId) {
            const coords = geo.split(',').map(Number);
            if (coords.length === 2 && !isNaN(coords[0]) && !isNaN(coords[1])) {
              myMap.setCenter(coords, 15, { duration: 300 });
              myMap.geoObjects.each(function(obj) {
                if (obj.properties.get('shopId') === shopId) {
                  obj.balloon.open();
                } else {
                  obj.balloon.close();
                }
              });

              // Обновляем активный магазин
              this.closest('.tabs__content-item').querySelectorAll('.shops__item').forEach(i => i.classList.remove('active'));
              this.classList.add('active');
            }
          }
        });
      });
    });
  }

  // Инициализация табов
  function initTabs(widgetId) {
    console.log('Инициализация табов для виджета: ' + widgetId);
    document.querySelectorAll(`.contacts-tabs [data-tab^="${widgetId}-region-"]`).forEach(item => {
      item.addEventListener('click', function() {
        const tabId = this.getAttribute('data-tab');
        const region = this.textContent.trim();
        const parent = this.closest('.contacts-tabs');

        // Убираем активный класс
        parent.querySelectorAll('.tabs__nav-item').forEach(i => i.classList.remove('active'));
        parent.querySelectorAll('.tabs__content-item').forEach(c => c.classList.remove('active'));

        // Добавляем активный класс
        this.classList.add('active');
        const targetTab = parent.querySelector('#tab-' + tabId);
        if (targetTab) {
          targetTab.classList.add('active');

          // Сбрасываем активный магазин
          targetTab.querySelectorAll('.shops__item').forEach(i => i.classList.remove('active'));

          // Показываем все точки для выбранного города
          const regionPoints = [];
          maps[widgetId].geoObjects.each(function(obj) {
            if (obj.properties.get('region') === region) {
              regionPoints.push(obj.geometry.getCoordinates());
            }
            obj.balloon.close();
          });

          if (regionPoints.length > 0) {
            if (regionPoints.length === 1) {
              maps[widgetId].setCenter(regionPoints[0], 15, { duration: 300 });
            } else {
              maps[widgetId].setBounds(ymaps.util.bounds.fromPoints(regionPoints), { checkZoomRange: true, duration: 300 });
            }
          }
        }
      });
    });
  }

  // Инициализация всех виджетов
  document.addEventListener('DOMContentLoaded', function() {
    console.log('Поиск контейнеров для Яндекс.Карт...');
    const mapElements = document.querySelectorAll('[id^="yandex-map-"]');
    if (mapElements.length === 0) {
      console.warn('Контейнеры для карт не найдены');
    }
    mapElements.forEach(mapElement => {
      const widgetId = mapElement.id.replace('yandex-map-', '');
      console.log('Найден контейнер карты для виджета: ' + widgetId);
      initYandexMap(widgetId);
      initTabs(widgetId);
    });
  });
})();

Ключевые особенности скрипта:

  • Инициализация карты: Создаёт карту с метками (islands#blackCircleDotIcon), которые не скрываются при открытии балуна.
  • Табы: При клике на город (tabs__nav-item) карта показывает все точки этого города с помощью setBounds.
  • Магазины: При клике на магазин (shops__item) карта фокусируется на его координатах (масштаб 15) и открывает балун с данными.
  • Отладка: Логирование в консоль для диагностики ошибок.

В файле Темы → Редактировать код → Layout → theme.liquid добавляем подключение API Яндекс.Карт и скрипта:

{{ 'yandex-map-init.js' | asset_url | script_tag }}
<script src="https://api-maps.yandex.ru/2.1/?apikey=YOUR_API_KEY&lang=ru_RU" type="text/javascript"></script>

Замените YOUR_API_KEY на ключ API, полученный в Yandex Developer Console. Убедитесь, что ваш домен добавлен в список разрешённых.

5. Вывод блока на нужной странице

  1. Перейдите в Контент → Страницы в админ-панели InSales.
  2. Выберите или создайте страницу (например, «Контакты»).
  3. В редакторе страницы добавьте виджет «Контакты с картой» через интерфейс InSales.
  4. Заполните данные в блоке «Магазины»:
    • Название: например, «Магазин 1».
    • Адрес: например, «ул. Примерная, 1» (без города).
    • Координаты: например, 55.792806,36.905985.
    • Телефон: например, +79991234567.
    • Город: выберите, например, Москва.
  5. Сохраните и опубликуйте страницу.

6. Тестирование и отладка

После настройки:

  • Откройте страницу в браузере и проверьте:
    • Табы с городами отображаются горизонтально.
    • Список магазинов показывает: название, адрес (например, г. Москва, ул. Примерная, 1), график (если есть), телефон (чёрная ссылка).
    • При клике на город карта показывает все точки этого города.
    • При клике на магазин карта фокусируется на нём, открывая балун с данными.
  • Проверьте консоль браузера (F12 → Console) на наличие ошибок.
  • Убедитесь, что JSON в <script id="map-coordinates-{{ widget.id }}"> корректен:
    [
      {
        "name": "Магазин 1",
        "address": "ул. Примерная, 1",
        "geo": "55.792806,36.905985",
        "shopId": "widget123-shop-0",
        "region": "Москва",
        "work": "Пн-Пт: 9:00-18:00",
        "phone": "+79991234567"
      }
    ]
    

Итог

Этот виджет предоставляет удобный способ отображения филиалов с интерактивной картой на платформе InSales. Он поддерживает:

  • Гибкую настройку данных магазинов через блоки.
  • Адаптивный дизайн для десктопов и мобильных устройств.
  • Интерактивную карту с фокусировкой на городах и магазинах.
  • Простое подключение через API Яндекс.Карт.

Если у вас возникнут вопросы или потребуется кастомизация (например, изменение стиля меток или балуна), напишите нам!

Готовы сделать всю работу за Вас
от4000
  • 120 минут

Перенесем сайты! Перенесем баланс! Подарим 3 месяца хостинга!
Диадок

Поработаем?

Опишите свой запрос, мы расчитаем стоимость вашей задачи.