(function () { 'use strict'; var TYPE_LABELS = { qr: 'QR', ean13: 'EAN-13', model: 'Модель', type: 'Тип', inventory: 'Инв. №', serial: 'S/N', custom: 'Текст', }; var DEMO = { model: 'Mikrotik RB952', type: 'Роутер', inventory: 'INV-DEMO-1', serial: 'S/N ABC123', custom: '', qr: 'QR', ean13: 'EAN-13', }; var dataEl = document.getElementById('label-template-data'); var state = { width_mm: 70, height_mm: 42, layout: [] }; try { state = JSON.parse(dataEl.textContent || '{}'); } catch (e) { /* keep defaults */ } if (!Array.isArray(state.layout)) state.layout = []; var canvas = document.getElementById('label-canvas'); var scaleInput = document.getElementById('tmpl-scale'); var widthInput = document.getElementById('tmpl-width'); var heightInput = document.getElementById('tmpl-height'); var statusEl = document.getElementById('status'); var selectedId = null; var drag = null; var uid = 1; state.layout.forEach(function (b) { if (b && b.id) { var n = parseInt(String(b.id).replace(/\D/g, ''), 10); if (n >= uid) uid = n + 1; } }); function scale() { return parseFloat(scaleInput.value) || 4; } function setStatus(msg, isErr) { statusEl.textContent = msg || ''; statusEl.className = isErr ? 'err' : ''; } function selectedBlock() { return state.layout.find(function (b) { return b.id === selectedId; }) || null; } function previewText(block) { if (block.type === 'custom') return block.text || 'Текст'; return DEMO[block.type] || block.type; } function render() { var s = scale(); canvas.style.width = (state.width_mm * s) + 'px'; canvas.style.height = (state.height_mm * s) + 'px'; canvas.innerHTML = ''; state.layout.forEach(function (block) { var el = document.createElement('div'); el.className = 'lbl-block' + (block.id === selectedId ? ' selected' : ''); el.dataset.id = block.id; el.style.left = (block.x_mm * s) + 'px'; el.style.top = (block.y_mm * s) + 'px'; el.style.width = (block.w_mm * s) + 'px'; el.style.height = (block.h_mm * s) + 'px'; el.style.fontSize = Math.max(8, block.font_pt * (s / 3.5)) + 'px'; el.style.fontWeight = block.bold ? '600' : '400'; el.style.textAlign = block.align || 'left'; el.innerHTML = '
' + (TYPE_LABELS[block.type] || block.type) + '
' + '
' + escapeHtml(previewText(block)) + '
' + '
'; canvas.appendChild(el); }); syncProps(); } function escapeHtml(str) { return String(str || '') .replace(/&/g, '&') .replace(//g, '>'); } function syncProps() { var block = selectedBlock(); var empty = document.getElementById('props-empty'); var fields = document.getElementById('props-fields'); if (!block) { empty.style.display = ''; fields.style.display = 'none'; return; } empty.style.display = 'none'; fields.style.display = ''; document.getElementById('prop-type-label').textContent = TYPE_LABELS[block.type] || block.type; document.getElementById('prop-x').value = block.x_mm; document.getElementById('prop-y').value = block.y_mm; document.getElementById('prop-w').value = block.w_mm; document.getElementById('prop-h').value = block.h_mm; document.getElementById('prop-font').value = block.font_pt; document.getElementById('prop-align').value = block.align || 'left'; document.getElementById('prop-bold').checked = !!block.bold; var customWrap = document.getElementById('prop-custom-wrap'); customWrap.style.display = block.type === 'custom' ? '' : 'none'; document.getElementById('prop-text').value = block.text || ''; } function clampBlock(block) { block.w_mm = Math.max(2, Number(block.w_mm) || 10); block.h_mm = Math.max(2, Number(block.h_mm) || 10); block.x_mm = Math.max(0, Math.min(state.width_mm - 2, Number(block.x_mm) || 0)); block.y_mm = Math.max(0, Math.min(state.height_mm - 2, Number(block.y_mm) || 0)); if (block.x_mm + block.w_mm > state.width_mm) block.w_mm = Math.max(2, state.width_mm - block.x_mm); if (block.y_mm + block.h_mm > state.height_mm) block.h_mm = Math.max(2, state.height_mm - block.y_mm); } function addBlock(type) { var defaults = { qr: { w_mm: 22, h_mm: 22 }, ean13: { w_mm: 50, h_mm: 14 }, model: { w_mm: 35, h_mm: 6, bold: true, font_pt: 10 }, type: { w_mm: 35, h_mm: 5, font_pt: 8 }, inventory: { w_mm: 35, h_mm: 5, font_pt: 10 }, serial: { w_mm: 35, h_mm: 4, font_pt: 8 }, custom: { w_mm: 30, h_mm: 5, text: 'Текст' }, }; var d = defaults[type] || { w_mm: 20, h_mm: 6 }; var block = { id: type + '-' + (uid++), type: type, x_mm: 2, y_mm: 2, w_mm: d.w_mm, h_mm: d.h_mm, font_pt: d.font_pt || 9, bold: !!d.bold, align: 'left', text: d.text || '', }; clampBlock(block); state.layout.push(block); selectedId = block.id; render(); } canvas.addEventListener('pointerdown', function (ev) { var target = ev.target.closest('.lbl-block'); if (!target) { selectedId = null; render(); return; } selectedId = target.dataset.id; var block = selectedBlock(); if (!block) return; var s = scale(); var isResize = !!ev.target.closest('[data-resize]'); drag = { id: block.id, resize: isResize, startX: ev.clientX, startY: ev.clientY, origX: block.x_mm, origY: block.y_mm, origW: block.w_mm, origH: block.h_mm, scale: s, pointerId: ev.pointerId, }; canvas.setPointerCapture(ev.pointerId); render(); ev.preventDefault(); }); canvas.addEventListener('pointermove', function (ev) { if (!drag) return; var block = state.layout.find(function (b) { return b.id === drag.id; }); if (!block) return; var dx = (ev.clientX - drag.startX) / drag.scale; var dy = (ev.clientY - drag.startY) / drag.scale; if (drag.resize) { block.w_mm = drag.origW + dx; block.h_mm = drag.origH + dy; } else { block.x_mm = drag.origX + dx; block.y_mm = drag.origY + dy; } clampBlock(block); render(); }); function endDrag() { drag = null; syncProps(); } canvas.addEventListener('pointerup', endDrag); canvas.addEventListener('pointercancel', endDrag); document.querySelectorAll('.palette [data-add]').forEach(function (btn) { btn.addEventListener('click', function () { addBlock(btn.getAttribute('data-add')); }); }); function bindProp(id, apply) { var el = document.getElementById(id); el.addEventListener('change', function () { var block = selectedBlock(); if (!block) return; apply(block, el); clampBlock(block); render(); }); el.addEventListener('input', function () { var block = selectedBlock(); if (!block) return; apply(block, el); clampBlock(block); render(); }); } bindProp('prop-x', function (b, el) { b.x_mm = parseFloat(el.value) || 0; }); bindProp('prop-y', function (b, el) { b.y_mm = parseFloat(el.value) || 0; }); bindProp('prop-w', function (b, el) { b.w_mm = parseFloat(el.value) || 2; }); bindProp('prop-h', function (b, el) { b.h_mm = parseFloat(el.value) || 2; }); bindProp('prop-font', function (b, el) { b.font_pt = parseFloat(el.value) || 9; }); bindProp('prop-align', function (b, el) { b.align = el.value; }); bindProp('prop-bold', function (b, el) { b.bold = el.checked; }); bindProp('prop-text', function (b, el) { b.text = el.value; }); document.getElementById('prop-delete').addEventListener('click', function () { if (!selectedId) return; state.layout = state.layout.filter(function (b) { return b.id !== selectedId; }); selectedId = null; render(); }); function resizeFromInputs() { state.width_mm = Math.max(20, Math.min(200, parseFloat(widthInput.value) || 70)); state.height_mm = Math.max(15, Math.min(200, parseFloat(heightInput.value) || 42)); widthInput.value = state.width_mm; heightInput.value = state.height_mm; state.layout.forEach(clampBlock); render(); } widthInput.addEventListener('change', resizeFromInputs); heightInput.addEventListener('change', resizeFromInputs); scaleInput.addEventListener('change', render); document.getElementById('btn-save').addEventListener('click', function () { var csrf = document.querySelector('meta[name="csrf-token"]'); var payload = { width_mm: state.width_mm, height_mm: state.height_mm, layout: state.layout, }; setStatus('Сохранение…'); fetch('/it-manager/equipment/labels/template/', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrf ? csrf.content : '', }, body: JSON.stringify(payload), }).then(function (r) { return r.json().then(function (data) { if (!r.ok || !data.ok) throw new Error((data && data.error) || 'Ошибка сохранения'); setStatus('Сохранено'); }); }).catch(function (err) { setStatus(err.message || 'Ошибка', true); }); }); render(); })();