Создание виджета с интерактивной картой филиалов/магазинов для InSales
от v2Team
В этой статье мы разберём, как создать виджет для платформы 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. Вывод блока на нужной странице
- Перейдите в Контент → Страницы в админ-панели InSales.
- Выберите или создайте страницу (например, «Контакты»).
- В редакторе страницы добавьте виджет «Контакты с картой» через интерфейс InSales.
- Заполните данные в блоке «Магазины»:
- Название: например, «Магазин 1».
- Адрес: например, «ул. Примерная, 1» (без города).
- Координаты: например,
55.792806,36.905985. - Телефон: например,
+79991234567. - Город: выберите, например,
Москва.
- Сохраните и опубликуйте страницу.
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 Яндекс.Карт.
Если у вас возникнут вопросы или потребуется кастомизация (например, изменение стиля меток или балуна), напишите нам!
Поработаем?
Опишите свой запрос, мы расчитаем стоимость вашей задачи.

