b4876c4efd
Co-authored-by: Cursor <cursoragent@cursor.com>
344 lines
13 KiB
JavaScript
344 lines
13 KiB
JavaScript
(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 {
|
|
var parsed = JSON.parse(dataEl.textContent || '{}');
|
|
// на случай старого двойного dumps → json_script
|
|
if (typeof parsed === 'string') parsed = JSON.parse(parsed);
|
|
if (parsed && typeof parsed === 'object') state = parsed;
|
|
} catch (e) { /* keep defaults */ }
|
|
if (!Array.isArray(state.layout)) state.layout = [];
|
|
state.width_mm = Number(state.width_mm) || 70;
|
|
state.height_mm = Number(state.height_mm) || 42;
|
|
|
|
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 csrfToken() {
|
|
var meta = document.querySelector('meta[name="csrf-token"]');
|
|
if (meta && meta.content) return meta.content;
|
|
var input = document.querySelector('input[name="csrfmiddlewaretoken"]');
|
|
if (input && input.value) return input.value;
|
|
var match = document.cookie.match(/(?:^|; )csrftoken=([^;]+)/);
|
|
return match ? decodeURIComponent(match[1]) : '';
|
|
}
|
|
|
|
function selectedBlock() {
|
|
return state.layout.find(function (b) { return b.id === selectedId; }) || null;
|
|
}
|
|
|
|
function previewText(block) {
|
|
if (block.type === 'custom') return block.text || 'Текст';
|
|
if (block.type === 'qr') {
|
|
var p = (block.text || '').trim();
|
|
if (!p) return 'инв. / EQ{id}';
|
|
return p.length > 28 ? p.slice(0, 28) + '…' : p;
|
|
}
|
|
return DEMO[block.type] || block.type;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return String(str || '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
}
|
|
|
|
function applyBlockStyle(el, block, s) {
|
|
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';
|
|
}
|
|
|
|
function render(opts) {
|
|
opts = opts || {};
|
|
var s = scale();
|
|
canvas.style.width = (state.width_mm * s) + 'px';
|
|
canvas.style.height = (state.height_mm * s) + 'px';
|
|
|
|
// во время drag только двигаем DOM, без пересоздания (иначе ломается pointer capture)
|
|
if (opts.moveOnly && drag) {
|
|
var moving = canvas.querySelector('.lbl-block[data-id="' + drag.id + '"]');
|
|
var block = state.layout.find(function (b) { return b.id === drag.id; });
|
|
if (moving && block) applyBlockStyle(moving, block, s);
|
|
return;
|
|
}
|
|
|
|
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;
|
|
applyBlockStyle(el, block, s);
|
|
el.innerHTML = '<div class="kind">' + (TYPE_LABELS[block.type] || block.type) + '</div>' +
|
|
'<div>' + escapeHtml(previewText(block)) + '</div>' +
|
|
'<div class="resize" data-resize="1"></div>';
|
|
canvas.appendChild(el);
|
|
});
|
|
if (!opts.skipProps) syncProps();
|
|
}
|
|
|
|
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 = round1(block.x_mm);
|
|
document.getElementById('prop-y').value = round1(block.y_mm);
|
|
document.getElementById('prop-w').value = round1(block.w_mm);
|
|
document.getElementById('prop-h').value = round1(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');
|
|
var qrHints = document.getElementById('prop-qr-hints');
|
|
var textLabel = document.getElementById('prop-text-label');
|
|
var showText = block.type === 'custom' || block.type === 'qr';
|
|
customWrap.style.display = showText ? '' : 'none';
|
|
if (qrHints) qrHints.style.display = block.type === 'qr' ? '' : 'none';
|
|
if (textLabel) {
|
|
textLabel.textContent = block.type === 'qr' ? 'Содержимое QR' : 'Текст';
|
|
}
|
|
document.getElementById('prop-text').value = block.text || '';
|
|
}
|
|
|
|
function round1(n) {
|
|
return Math.round(Number(n) * 10) / 10;
|
|
}
|
|
|
|
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, text: '{inventory}' },
|
|
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,
|
|
};
|
|
try { canvas.setPointerCapture(ev.pointerId); } catch (e) { /* ignore */ }
|
|
// обновить выделение без сброса pointer
|
|
canvas.querySelectorAll('.lbl-block').forEach(function (el) {
|
|
el.classList.toggle('selected', el.dataset.id === selectedId);
|
|
});
|
|
syncProps();
|
|
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({ moveOnly: true });
|
|
});
|
|
|
|
function endDrag(ev) {
|
|
if (!drag) return;
|
|
if (ev && drag.pointerId != null) {
|
|
try { canvas.releasePointerCapture(drag.pointerId); } catch (e) { /* ignore */ }
|
|
}
|
|
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({ skipProps: true });
|
|
});
|
|
}
|
|
|
|
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 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': csrfToken(),
|
|
},
|
|
body: JSON.stringify(payload),
|
|
}).then(function (r) {
|
|
return r.json().then(function (data) {
|
|
if (!r.ok || !data.ok) throw new Error((data && data.error) || ('Ошибка ' + r.status));
|
|
setStatus('Сохранено');
|
|
});
|
|
}).catch(function (err) {
|
|
setStatus(err.message || 'Ошибка', true);
|
|
});
|
|
});
|
|
|
|
widthInput.value = state.width_mm;
|
|
heightInput.value = state.height_mm;
|
|
render();
|
|
})();
|