Изометрия в 1С: склад стал интереснее, чем ваш сериал
- пятница, 26 июня 2026 г. в 00:00:11
Что, если мы создадим такой интерфейс в 1С, чтобы он был удобнее, чем в Excel? Да не просто удобнее — а чтобы сотрудники сказали: «Ого, это же как игра!».
С вами снова Ведущий специалист модуля разработки 1С Михеев Антон. Давайте вместе сделаем эту игру идею реальностью.
Представьте, что Excel — это склад, вид сверху. Синие ячейки —стеллажи, в них лежат товары. Да, на множестве складов топология нарисована именно в Excel. Сотрудникам так понятнее и удобнее: закрашивать ячейки в таблице куда проще, чем разбираться, как заполнять справочники с кучей непонятных цифр в 1С.

Задачка: на стеллаже три полки. На первой — конфеты, на второй — печеньки, на третьей — сиропы. Как пользователю понять, что нужно взять печеньку со второй полки?
Ответ: рисовать всю эту красоту в изометрии! Изометрия — это старая добрая технология, которая:
не нагружает процессор (в отличие от 3D, где ваш ПК может начать мечтать о пенсии);
позволяет рассмотреть предмет под углом в 30∘;
даёт возможность переключать углы обзора (ну, или хотя бы сделать четыре фиксированных вида — этого хватит).

Здесь нам поможет встроенный редактор HTML внутри 1С. Если вы не знали, то 1С использует платформу WebKit, позаимствованную у айфонов (лучшие мировые практики, да).
Шаг 1. Создаём внешнюю обработку и добавляем в неё Поле HTML документа.

Шаг 2. Создаём три макета: HTML, JS и CSS.

Макет HTML — это каркас страницы. Вставляем код:
<!DOCTYPE html> <HTML> <HEAD>@CSS</HEAD> <BODY> <h1>Изометрическая сетка 10 × 10 с перетаскиваемыми объектами</h1> <div id='grid-container'></div> @JS </BODY> </HTML>
Макет JS — это логика. Здесь мы:
создаём сетку из ячеек;
расставляем объекты (конфеты, печеньки, сиропы);
добавляем возможность перетаскивать их мышкой (drag & drop);
подсвечиваем ячейки при наведении;
проверяем, занята ли ячейка, прежде чем положить туда новый объект.

window.onload = function() { const gridContainer = document.getElementById('grid-container'); const occupiedCells = new Set(); // Хранит занятые ячейки // Массив контейнеров с параметрами const containers = [@Containers]; // Создаём ячейки сетки for (let i = 0; i < 10; i++) { for (let j = 0; j < 10; j++) { const cell = document.createElement('div'); cell.className = 'cell'; cell.style.left = ${j * 90}px; cell.style.top = ${i * 50}px; cell.dataset.row = i; cell.dataset.col = j; // Добавляем параметры x и y в качестве атрибутов ячейки cell.setAttribute('data-x', i); cell.setAttribute('data-y', j); gridContainer.appendChild(cell); } } // Расставляем объекты из массива контейнеров containers.forEach(container => { const item = document.createElement('div'); item.className = 'draggable-item'; item.textContent = container.text; item.draggable = true; item.style.backgroundColor = container.color; item.style.zIndex = parseInt(container.x / container.y * 100,10); const Top = document.createElement('div'); Top.className = 'top'; Top.textContent = container.text; Top.style.backgroundColor = container.color; const Front = document.createElement('div'); Front.className = 'front'; Front.style.backgroundColor = container.color; const Right = document.createElement('div'); Right.className = 'right'; Right.textContent = container.text; Right.style.backgroundColor = container.color; item.appendChild(Top); item.appendChild(Front); item.appendChild(Right); // Добавляем все параметры в качестве атрибутов draggable-item item.setAttribute('data-color', container.color); item.setAttribute('data-text', container.text); item.setAttribute('data-x', container.x); item.setAttribute('data-y', container.y); item.setAttribute('data-z', container.z); // Помещаем объект в ячейку согласно координатам x и y const targetCell = document.querySelector(.cell[data-row="${container.x}"][data-col="${container.y}"]); if (targetCell) { targetCell.appendChild(item); // Помечаем ячейку как занятую occupiedCells.add(${container.x},${container.y}); } }); let draggedItem = null; let originalCell = null; // Обработчик начала перетаскивания function handleDragStart(e) { draggedItem = this; originalCell = this.parentElement; e.dataTransfer.setData('text/plain', null); // Невидимость Ghost при перетаскивании var img = document.createElement("img"); img.src = ""; e.dataTransfer.setDragImage(img, 0, 0); } // Обработчик окончания перетаскивания document.addEventListener('dragend', () => { if (draggedItem) { draggedItem = null; originalCell = null; } }); // Обработчики для всех ячеек сетки document.querySelectorAll('.cell').forEach(cell => { // Обработчик наведения — выделение фона и границы cell.addEventListener('mouseenter', () => { cell.style.backgroundColor = 'rgba(135, 206, 235, 0.3)'; // Полупрозрачный голубой cell.style.borderColor = 'blue'; cell.style.zIndex = '10'; // Выводим поверх других элементов cell.style.boxShadow = '0 0 8px rgba(0, 123, 255, 0.5)'; // Дополнительная подсветка }); cell.addEventListener('mouseleave', () => { // Восстанавливаем исходный вид cell.style.backgroundColor = ''; cell.style.borderColor = '#999'; cell.style.zIndex = ''; cell.style.boxShadow = ''; }); cell.addEventListener('dragover', (e) => e.preventDefault()); cell.addEventListener('drop', (e) => { e.preventDefault(); if (!draggedItem) return; const row = cell.dataset.row; const col = cell.dataset.col; const cellKey = ${row},${col}; // Проверяем, занята ли ячейка if (occupiedCells.has(cellKey)) { alert('Ячейка уже занята! Выберите другую.'); return; } // Освобождаем предыдущую ячейку if (originalCell) { const origRow = originalCell.dataset.row; const origCol = originalCell.dataset.col; occupiedCells.delete(${origRow},${origCol}); } // Обновляем атрибуты draggable-item с новыми координатами draggedItem.setAttribute('data-x', row); draggedItem.setAttribute('data-y', col); // Помещаем объект в новую ячейку — он автоматически занимает всю площадь ячейки cell.appendChild(draggedItem); occupiedCells.add(cellKey); }); }); // Инициализируем обработчики перетаскивания для всех объектов на поле document.querySelectorAll('.draggable-item').forEach(item => { item.addEventListener('dragstart', handleDragStart); }); document.querySelectorAll('.draggable-item').forEach(item => { item.addEventListener('mouseenter', () => { document.querySelectorAll('.draggable-item').forEach(otherItem => { if (otherItem !== item) { otherItem.classList.add('dimmed'); } }); }); item.addEventListener('mouseleave', () => { document.querySelectorAll('.draggable-item').forEach(otherItem => { otherItem.classList.remove('dimmed'); }); }); }); // Запрещаем контекстное меню по правой кнопке мыши document.addEventListener('contextmenu', e => e.preventDefault()); }
Макет CSS — это стиль. Здесь мы задаём цвета, размеры, тени, углы поворота и прочие красоты. Изометрия достигается через transform: rotate(-30deg) skewX(30deg) — магия, а не код!
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; text-align: center; } #grid-container { width: 1000px; height: 500px; position: relative; transform: rotate(-30deg) skewX(30deg); overflow: hidden; margin: 0 auto; } .cell { position: absolute; width: 90px; height: 48px; border-right: 1px dashed #999; border-bottom: 1px dashed #999; box-sizing: border-box; } /* Границы для крайних ячеек */ .cell[data-row="0"] { border-top: 1px dashed #999; } .cell[data-col="0"] { border-left: 1px dashed #999; } .draggable-item { width: 100%; height: 100%; background-color: #4CAF50; color: white; text-align: center; line-height: 48px; /* Совпадает с высотой ячейки */ cursor: move; user-select: none; border-radius: 4px; position: relative; /* Относительное позиционирование внутри ячейки */ } .draggable-item.dimmed { opacity: 0.5; transition: opacity 0.2s ease; /* Плавное изменение прозрачности */ } .top{ filter: brightness(120%); background-color: #4CAF50; color: white; transform: translateX(30px) translateY(-80px); position: absolute; width: 90px; height: 48px; } .front{ filter: brightness(90%); color: white; transform: translateY(-66px) skewY(-45deg) ; position: absolute; width: 30px; height: 48px; } .right{ filter: brightness(110%); color: white; transform: translateX(15px) translateY(-33px) skewX(-45deg) ; position: absolute; width: 90px; height: 33px; }
Код на сервере: магия данных
В обработчике ПриСозданииНаСервере пишем код, который:
Берёт макеты HTML, JS и CSS.
Подставляет CSS и JS в HTML (через @CSS и @JS).
Делает запрос к регистру накопления, чтобы получить координаты ячеек и информацию о контейнерах.
Формирует массив контейнеров с параметрами (цвет, текст, координаты).
Подставляет этот массив в HTML (вместо @Containers).
Добавим код ПриСозданииНаСервере:
bsl ОбъектВнешнейОбработки = РеквизитФормыВЗначение("Объект"); HTML = ОбъектВнешнейОбработки.ПолучитьМакет("МакетHTML").ПолучитьТекст(); JS = "<script>" + ОбъектВнешнейОбработки.ПолучитьМакет("МакетJS").ПолучитьТекст() + "</script>"; CSS = "<style>" + ОбъектВнешнейОбработки.ПолучитьМакет("МакетCSS").ПолучитьТекст() + "</style>"; HTML = СтрЗаменить(HTML,"@CSS",CSS); HTML = СтрЗаменить(HTML,"@JS",JS); //Тут пишем запрос который получит Координаты ячейки и Контейнер в ней Запрос = Новый Запрос; Запрос.Текст = "ВЫБРАТЬ РАЗЛИЧНЫЕ | укЗаполненностьЯчеекОстатки.Ячейка.Ряд КАК X, | укЗаполненностьЯчеекОстатки.Ячейка.Ярус КАК Z, | укЗаполненностьЯчеекОстатки.Ячейка.Позиция КАК Y |ИЗ | РегистрНакопления.укЗаполненностьЯчеек.Остатки КАК укЗаполненностьЯчеекОстатки |ГДЕ | укЗаполненностьЯчеекОстатки.Ячейка.Ряд < 10 | И укЗаполненностьЯчеекОстатки.Ячейка.Ярус = 1 | И укЗаполненностьЯчеекОстатки.Ячейка.Позиция < 10 | |УПОРЯДОЧИТЬ ПО | X Возр, | Y ВОЗР"; РезультатЗапроса = Запрос.Выполнить(); Выборка = РезультатЗапроса.Выбрать(); Результат = ""; Пока Выборка.Следующий() Цикл ТекСтрока = СтрШаблон("{ color: '#00008b', text: '%1', x: %2, y: %3, z: %4 },", "К-"+ Выборка.X + "-" + Выборка.Y , Выборка.X, Выборка.Y, Выборка.Z); Результат = Результат + ТекСтрока; КонецЦикла; Результат = Лев(Результат, СтрДлина(Результат) - 1); HTML = СтрЗаменить(HTML,"@Containers", Результат);
«Запрос в 1С — это как заклинание: если произнести правильно, получишь желаемое. Если нет — получишь ошибку и желание всё бросить».
На выходе получается склад с изометрией! Ого, скажете вы, и будете правы. Это реально работает внутри 1С и не тормозит (в отличие от 3D, которое может заставить ваш компьютер задуматься о смысле жизни).
Что можно добавить:
Подсветку ячеек: красным — «Возьми отсюда», зелёным — «Поставь сюда».
Динамическую подгрузку задач на перемещение.
Отображение остатков прямо на контейнерах.
Уведомления: «Внимание! На полке с печеньками осталось 2 штуки!».
Получается уже не просто 1С, а почти игра!

Вы спросите: «А почему не использовать 3D?» Ответ прост:
1С не сможет быстро выполнять качественный рендеринг моделей — всё будет тормозить.
Оптимизация 3D‑движка в 1С не даёт качественной детализации.
Возможность крутить камеру только сбивает пользователя.
«3D в 1С — это как попытка запустить Cyberpunk 2077 на калькуляторе: идея крутая, но результат печальный».
Зафиксируем угол для изометрии и прорисуем все объекты лишь под одним углом (максимум 4 картинки на объект). И вот он — весь склад перед вами, с хорошей детализацией, без тормозов и без необходимости изучать квантовую физику.


Как вам идейка? Удобно ли будет пользоваться детализированной интерактиивной топологией склада? 😉
