it_manager/locations: редактор шаблона этикеток, раздельная печать QR/EAN и карточка поставщика.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -151,11 +151,13 @@ function itBulkClearSelection() {
|
||||
itBulkUpdate();
|
||||
}
|
||||
|
||||
function itEquipmentOpenLabels() {
|
||||
function itEquipmentOpenLabels(codes) {
|
||||
var checked = Array.prototype.slice.call(document.querySelectorAll('.it-equip-check:checked'));
|
||||
if (!checked.length) return;
|
||||
var ids = checked.map(function (cb) { return cb.value; });
|
||||
var url = '/it-manager/equipment/labels/?ids=' + encodeURIComponent(ids.join(','));
|
||||
var mode = codes || 'both';
|
||||
var url = '/it-manager/equipment/labels/?ids=' + encodeURIComponent(ids.join(',')) +
|
||||
'&codes=' + encodeURIComponent(mode);
|
||||
window.open(url, '_blank', 'noopener');
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
(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 = '<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);
|
||||
});
|
||||
syncProps();
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str || '')
|
||||
.replace(/&/g, '&')
|
||||
.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();
|
||||
})();
|
||||
Reference in New Issue
Block a user