Por favor, verifique a imagem, no final dos número...
創建於:2025年8月26日
使用 GPT-5 Thinking 回答 Chat01
創建於:2025年8月26日
使用 GPT-5 Thinking 回答 Chat01
Por favor, verifique a imagem, no final dos números tem um espaço quando estou lendo as etiquetas que foram geradas, preciso que isso não aconteça, analise profundamente o codigo e corrija para que a sequencia de numero fique uma a baixo da outra mas sem esse espaço no final, me entregue o codigo corrigido e completo ao final da analise.
<!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Gerador de Etiquetas (Versão Profissional)</title> <style> :root { --primary-color: #007bff; --secondary-color: #6c757d; --success-color: #28a745; --danger-color: #dc3545; --light-bg-color: #f8f9fa; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --border-radius: 0.25rem; --card-shadow: 0 4px 8px rgba(0,0,0,0.05); } body { font-family: var(--font-family); background-color: #f4f7f9; color: #343a40; margin: 0; padding: 1.5rem; display: flex; justify-content: center; } .container { width: 100%; max-width: 1200px; } h1, h2 { color: var(--primary-color); text-align: center; } h1 { margin-bottom: 2rem; } h2 { margin-bottom: 1.5rem; text-align: left; border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem;} .card { background-color: #fff; border: 1px solid #dee2e6; border-radius: var(--border-radius); padding: 2rem; margin-bottom: 1.5rem; box-shadow: var(--card-shadow); } .tab-container { display: flex; border-bottom: 1px solid #dee2e6; margin-bottom: 1.5rem; } .tab-link { padding: 0.75rem 1.25rem; cursor: pointer; border: none; background-color: transparent; font-size: 1rem; font-weight: 500; color: var(--secondary-color); border-bottom: 3px solid transparent; transition: all 0.2s ease; } .tab-link:hover { color: var(--primary-color); } .tab-link.active { color: var(--primary-color); border-bottom-color: var(--primary-color); } .tab-content { display: none; } .tab-content.active { display: block; } .btn { display: inline-flex; align-items: center; gap: 0.5rem; font-weight: 500; color: #fff; text-align: center; cursor: pointer; user-select: none; background-color: var(--primary-color); border: 1px solid var(--primary-color); padding: 0.6rem 1.2rem; font-size: 1rem; border-radius: var(--border-radius); transition: all 0.15s ease-in-out; text-decoration: none; } .btn:hover { opacity: 0.85; } .btn-secondary { background-color: var(--secondary-color); border-color: var(--secondary-color); } .btn-success { background-color: var(--success-color); border-color: var(--success-color); } .file-upload-wrapper { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; } input[type="file"] { display: none; } .file-upload-label { border: 1px solid var(--primary-color); color: var(--primary-color); padding: 0.6rem 1.2rem; border-radius: var(--border-radius); cursor: pointer; transition: all 0.2s ease;} .file-upload-label:hover { background-color: var(--primary-color); color: #fff; } .file-name { font-style: italic; color: var(--secondary-color); } textarea.input-area { width: 100%; min-height: 150px; padding: 0.75rem; font-family: "Courier New", Courier, monospace; font-size: 1rem; border-radius: var(--border-radius); border: 1px solid #ccc; box-sizing: border-box; } select { padding: 0.6rem; border-radius: var(--border-radius); border: 1px solid #ccc; font-size: 1rem; } .status-box { margin-top: 1.5rem; padding: 1rem; border-radius: var(--border-radius); display: none; word-wrap: break-word; } .status-box.success { border: 1px solid #c3e6cb; background-color: #d4edda; color: #155724; } .status-box.error { border: 1px solid #f5c6cb; background-color: #f8d7da; color: #721c24; } .status-box.info { border: 1px solid #bde5f8; background-color: #cce5ff; color: #004085; } .loader { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary-color); border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-right: 0.75rem; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .label-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1.5rem; } .label-card { border: 1px solid #ccc; border-radius: var(--border-radius); padding: 1rem; background: #fff; display: flex; flex-direction: column; align-items: center; min-height: 150px; position: relative; padding-top: 2.5rem; } .label-card h3 { margin-top: 0; font-size: 1.1rem; word-break: break-all; text-align: center; } .label-card p { margin: 0.25rem 0; font-size: 0.9rem; } .label-card canvas { max-width: 100%; height: auto; margin-top: 1rem; border: 1px solid #eee; } .label-card pre { white-space: pre-wrap; word-wrap: break-word; font-family: var(--font-family); font-size: 0.95rem; margin: 0; background-color: var(--light-bg-color); padding: 1rem; border-radius: var(--border-radius); border: 1px solid #eee; width: 100%; box-sizing: border-box; } .results-controls { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #dee2e6; margin-bottom: 1.5rem; } .download-buttons { display: flex; gap: 1rem; flex-wrap: wrap; } .label-actions { position: absolute; top: 0.5rem; right: 0.5rem; display: flex; gap: 0.5rem; background-color: rgba(255,255,255,0.8); border-radius: 5px; padding: 2px; } .action-btn { background: none; border: none; cursor: pointer; padding: 0.3rem; opacity: 0.6; transition: opacity 0.2s ease; } .action-btn:hover { opacity: 1; } .action-btn svg { width: 18px; height: 18px; display: block; } .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); display: none; justify-content: center; align-items: center; z-index: 1000; } .modal-content { background: #fff; padding: 2rem; border-radius: var(--border-radius); max-width: 90%; width: 500px; max-height: 90%; overflow-y: auto; text-align: center; position: relative; } .modal-close { position: absolute; top: 0.5rem; right: 1rem; font-size: 2rem; font-weight: bold; cursor: pointer; color: #555; } .modal-body { margin-top: 1rem; } .modal-body canvas { max-width: 100%; } .modal-body pre { text-align: left; } </style> </head> <body></body> </html>text<div class="container"> <div style="display: flex; justify-content: center; align-items: center; position: relative;"> <h1>Gerador de Etiquetas</h1> <button id="settings-trigger-btn" aria-label="Abrir Configurações de Impressão" title="Configurações de Impressão" style="position: absolute; right: 0; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; padding: 0.5rem; opacity: 0.7; transition: opacity 0.2s;"> <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#6c757d"> <title>Ícone de Configurações</title> <g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M4 12H20M4 6H20M4 18H20" stroke="#6c757d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g> </svg> </button> </div> <div class="card"> <h2>1. Entrada de Dados</h2> <div class="tab-container"> <button class="tab-link active" data-tab="excel">Arquivo Excel</button> <button class="tab-link" data-tab="xml">Arquivo XML</button> <button class="tab-link" data-tab="manual">Digitação Manual</button> </div> <div id="excelTab" class="tab-content active"> <p>Selecione um arquivo Excel (.xlsx).</p> <div class="file-upload-wrapper"> <label for="excelFileInput" class="file-upload-label">Escolher Arquivo</label> <input type="file" id="excelFileInput" accept=".xlsx,.xls"> <span id="excelFileName" class="file-name">Nenhum arquivo selecionado</span> </div> <div style="margin-top: 1rem;"> <button id="processExcelBtn" class="btn" disabled>Processar Excel</button> </div> </div> <div id="xmlTab" class="tab-content"> <p>Selecione um ou mais arquivos XML de NF-e.</p> <div class="file-upload-wrapper"> <label for="xmlFileInput" class="file-upload-label">Escolher Arquivos</label> <input type="file" id="xmlFileInput" accept=".xml, text/xml" multiple> <span id="xmlFileName" class="file-name">Nenhum arquivo selecionado</span> </div> </div> <div id="manualTab" class="tab-content"> <p>Digite ou cole o conteúdo que deseja na etiqueta.</p> <textarea id="manualInput" class="input-area" placeholder="Nota fiscal: ..."></textarea> <div style="margin-top: 1rem; display: flex; align-items: center; gap: 1rem;"> <button id="processManualBtn" class="btn">Gerar Etiqueta</button> <select id="manualOutputType"> <option value="text_only" selected>Somente Escrita</option> <option value="text_and_code">Escrita e Código</option> <option value="code_only">Somente Código</option> </select> </div> </div> <div id="statusBox" class="status-box"></div> </div> <div id="excelResultsCard" class="card" style="display: none;"> <h2>Resultados do Excel</h2> <div class="results-controls"> <div class="output-selection-wrapper"> <label for="excelOutputFormatSelect"><strong>Tipo de Saída:</strong></label> <select id="excelOutputFormatSelect"></select> </div> <div id="excelDownloadButtons" class="download-buttons"></div> </div> <div id="excelResults" class="label-grid"></div> </div> <div id="xmlResultsCard" class="card" style="display: none;"> <h2>Resultados do XML</h2> <div class="results-controls"> <div class="output-selection-wrapper"> <label for="xmlOutputFormatSelect"><strong>Tipo de Saída:</strong></label> <select id="xmlOutputFormatSelect"></select> </div> <div id="xmlDownloadButtons" class="download-buttons"></div> </div> <div id="xmlResults" class="label-grid"></div> </div> <div id="manualResultsCard" class="card" style="display: none;"> <h2>Resultados da Digitação Manual</h2> <div class="results-controls"> <div class="output-selection-wrapper"> <label for="manualOutputFormatSelect"><strong>Tipo de Saída:</strong></label> <select id="manualOutputFormatSelect"></select> </div> <div id="manualDownloadButtons" class="download-buttons"></div> </div> <div id="manualResults" class="label-grid"></div> </div> </div> <div id="modal-view" class="modal-overlay"> <div class="modal-content"> <span class="modal-close">×</span> <div id="modal-body" class="modal-body"></div> </div> </div> <div id="settings-modal" class="modal-overlay"> <div class="modal-content"> <span id="settings-modal-close" class="modal-close">×</span> <h2>Configurações de Impressão</h2> <div class="settings-body" style="margin-top: 1.5rem; text-align: left;"> <label for="barcodeTypeSelect"><strong>Tipo de Código de Barras (Excel e Manual):</strong></label> <select id="barcodeTypeSelect" style="width: 100%; margin-top: 0.5rem;"> <option value="pdf417">PDF417</option> <option value="code128">Code 128</option> <option value="qrcode">QR Code</option> </select> <p style="font-size: 0.8rem; color: #6c757d; margin-top: 1rem;"> Atenção: Code 128 não suporta múltiplas linhas. Apenas a primeira linha de dados será usada. </p> </div> </div> </div> <script src="https://cdn.sheetjs.com/xlsx-0.19.3/package/dist/xlsx.full.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/bwip-js/3.2.1/bwip-js.min.js"></script> <script> document.addEventListener('DOMContentLoaded', function () { // Verifica se as bibliotecas essenciais foram carregadas if (typeof XLSX === 'undefined' || typeof jspdf === 'undefined' || typeof bwipjs === 'undefined') { document.getElementById('statusBox').innerHTML = '<strong>Erro Crítico:</strong> Bibliotecas externas não foram carregadas. Verifique sua conexão com a internet.'; return; } // --- ELEMENTOS GLOBAIS --- const statusBox = document.getElementById('statusBox'); const modalView = document.getElementById('modal-view'); const modalBody = document.getElementById('modal-body'); const modalClose = document.querySelector('#modal-view .modal-close'); const settingsTriggerBtn = document.getElementById('settings-trigger-btn'); const settingsModal = document.getElementById('settings-modal'); const settingsModalClose = document.getElementById('settings-modal-close'); const barcodeTypeSelect = document.getElementById('barcodeTypeSelect'); // --- CONFIGURAÇÕES E FUNÇÕES GLOBAIS DE DOWNLOAD --- const outputFormats = { 'pdf-a4': { name: 'Folha A4 (Layout em Lista)', type: 'pdf' }, 'zpl-100x150': { name: 'Zebra (100 x 150mm)', type: 'nblb', width: 100, height: 150 }, 'zpl-100x70': { name: 'Zebra (100 x 70mm)', type: 'nblb', width: 100, height: 70 }, 'zpl-100x50': { name: 'Zebra (100 x 50mm)', type: 'nblb', width: 100, height: 50 }, 'zpl-70x50': { name: 'Zebra (70 x 50mm)', type: 'nblb', width: 70, height: 50 }, 'zpl-70x30': { name: 'Zebra (70 x 30mm)', type: 'nblb', width: 70, height: 30 } }; function populateOutputSelect(selectElement) { selectElement.innerHTML = ''; const pdfGroup = document.createElement('optgroup'); pdfGroup.label = 'PDF Padrão'; const zplGroup = document.createElement('optgroup'); zplGroup.label = 'Impressora Zebra'; for (const key in outputFormats) { const format = outputFormats[key]; const option = document.createElement('option'); option.value = key; option.textContent = format.name; if (format.type === 'pdf') pdfGroup.appendChild(option); else zplGroup.appendChild(option); } selectElement.appendChild(pdfGroup); selectElement.appendChild(zplGroup); } function showModalWithContent(contentNode) { modalBody.innerHTML = ''; modalBody.appendChild(contentNode); modalView.style.display = 'flex'; } // --- Funções de Download --- function downloadPdfA4(data) { if (data.length === 0) return; const { jsPDF } = window.jspdf; const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' }); const pageMargin = 15; let currentY = pageMargin; data.forEach((item) => { const bcid = barcodeTypeSelect.value; const labelHeight = bcid === 'pdf417' ? 40 : 50; if (currentY + labelHeight > pdf.internal.pageSize.getHeight() - pageMargin) { pdf.addPage(); currentY = pageMargin; } if(item.title) pdf.setFontSize(9).text(item.title, pageMargin, currentY + 5); if (item.generateBarcode) { const canvas = document.createElement('canvas'); let text = item.barcodeData; if(bcid === 'code128') text = text.split('\n')[0].split('\r')[0]; bwipjs.toCanvas(canvas, { bcid, text, eclevel: bcid === 'pdf417' ? 2 : 'L' }); const imgHeight = bcid === 'pdf417' ? 25 : (bcid === 'qrcode' ? 35 : 20); const imgWidth = bcid === 'pdf417' ? 80 : (bcid === 'qrcode' ? 35 : 80); pdf.addImage(canvas.toDataURL('image/png'), 'PNG', pageMargin, currentY + 8, imgWidth, imgHeight); } else if (item.barcodeData) { const maxWidth = pdf.internal.pageSize.getWidth() - (pageMargin * 2); const maxHeight = labelHeight - 10; let fontSize = 9; let dimensions; do { pdf.setFontSize(fontSize); dimensions = pdf.getTextDimensions(item.barcodeData, { maxWidth: maxWidth }); if (dimensions.h > maxHeight && fontSize > 4) { fontSize -= 0.5; } } while (dimensions.h > maxHeight && fontSize > 4); pdf.setFontSize(fontSize); pdf.text(item.barcodeData, pageMargin, currentY + (item.title ? 12 : 5), { maxWidth: maxWidth }); } pdf.line(pageMargin, currentY + labelHeight - 2, pdf.internal.pageSize.getWidth() - pageMargin, currentY + labelHeight - 2); currentY += labelHeight; }); pdf.save(`etiquetas_A4_${new Date().toISOString().slice(0,10)}.pdf`); } function downloadPdfZebraPreview(data, format) { if (data.length === 0) return; const { jsPDF } = window.jspdf; const orientation = format.width > format.height ? 'l' : 'p'; const pdf = new jsPDF({ orientation, unit: 'mm', format: [format.width, format.height] }); data.forEach((item, index) => { if (index > 0) pdf.addPage([format.width, format.height], orientation); const margin = 3; if(item.generateBarcode) { const canvas = document.createElement('canvas'); const bcid = barcodeTypeSelect.value; let text = item.barcodeData; if(bcid === 'code128') text = text.split('\n')[0].split('\r')[0]; bwipjs.toCanvas(canvas, { bcid, text, eclevel: bcid === 'pdf417' ? 2 : 'L' }); const titleHeight = item.title ? 8 : 0; const availableWidth = format.width - (margin * 2); const availableHeight = format.height - titleHeight - (margin * 2); let finalWidth = availableWidth; let finalHeight = (canvas.height * finalWidth) / canvas.width; if (finalHeight > availableHeight) { finalHeight = availableHeight; finalWidth = finalHeight * canvas.width / canvas.height; } const totalContentHeight = titleHeight + finalHeight; const startY = (format.height - totalContentHeight) / 2; const xPos = margin + (availableWidth - finalWidth) / 2; if(item.title) pdf.setFontSize(9).text(item.title, format.width / 2, startY + 4, { align: 'center' }); pdf.addImage(canvas.toDataURL('image/png'), 'PNG', xPos, startY + titleHeight, finalWidth, finalHeight); } else if (item.barcodeData) { const maxWidth = format.width - (margin * 2); const maxHeight = format.height - (margin * 2); let fontSize = 9; let dimensions; do { pdf.setFontSize(fontSize); dimensions = pdf.getTextDimensions(item.barcodeData, { maxWidth: maxWidth }); if (dimensions.h > maxHeight && fontSize > 4) { fontSize -= 0.5; } } while (dimensions.h > maxHeight && fontSize > 4); const startY = (format.height - dimensions.h) / 2 + margin; pdf.setFontSize(fontSize); pdf.text(item.barcodeData, margin, startY, { maxWidth: maxWidth }); } }); pdf.save(`preview_${format.width}x${format.height}.pdf`); } function downloadNblb(data, format) { if (data.length === 0) return; const dotsPerMm = 8; const labelWidthDots = Math.round(format.width * dotsPerMm); const marginDots = 24; let zplCommands = ""; const bcid = barcodeTypeSelect.value; data.forEach(item => { const cleanTitle = item.title ? item.title.replace(/[\^~]/g, '') : 'Etiqueta'; const barcodeData = item.barcodeData.replace(/[\^~]/g, ''); zplCommands += `^XA\n^CI28\n`; if (item.generateBarcode) { if(item.title) zplCommands += `^FO${marginDots},${marginDots}^A0N,28,28^FD${cleanTitle}^FS\n`; const yPos = item.title ? marginDots + 40 : marginDots; if (bcid === 'qrcode') { zplCommands += `^FO${marginDots + 20},${yPos}^BQN,2,5^FDQA,${barcodeData}^FS\n`; } else if (bcid === 'code128') { const firstLine = barcodeData.split('\n')[0].split('\r')[0]; zplCommands += `^BY2,3,60^FO${marginDots},${yPos}^BCN,,N,N^FD${firstLine}^FS\n`; } else { // pdf417 zplCommands += `^FO${marginDots},${yPos}^B7N,5,3,,,N^FD${barcodeData}^FS\n`; } } else { zplCommands += `^FO${marginDots},${marginDots}^FB${labelWidthDots - (marginDots * 2)},20,0,L,0^A0N,24,24^FD${barcodeData}^FS\n`; } zplCommands += `^XZ\n\n`; }); const blob = new Blob([zplCommands], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `etiquetas_${format.width}x${format.height}mm_${new Date().toISOString().slice(0,10)}.nblb`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function downloadSummaryExcel(data) { const exportData = data.map(group => ({ 'Caixa Master': group.caixaMaster, 'NF': group.nf === 'S/N' ? '' : group.nf, 'Quantidade de IMEIs': group.imeis.length, 'Lista de IMEIs': group.imeis.join(';') })); const worksheet = XLSX.utils.json_to_sheet(exportData); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "Resumo Agrupado"); XLSX.writeFile(workbook, `resumo_etiquetas_${new Date().toISOString().slice(0,10)}.xlsx`); } // --- FIM Funções de Download --- // --- FÁBRICA DE APLICAÇÃO DE ETIQUETAS (LÓGICA REUTILIZÁVEL) --- function createLabelApp(config) { let data = []; const resultsDiv = document.getElementById(config.resultsDivId); const resultsCard = document.getElementById(config.resultsCardId); const outputFormatSelect = document.getElementById(config.outputFormatSelectId); const downloadButtonsDiv = document.getElementById(config.downloadButtonsDivId); function getData() { return data; } function setData(newData) { data = newData; } function display() { resultsDiv.innerHTML = ''; if (data.length === 0) { resultsCard.style.display = 'none'; return; } data.forEach(item => { const card = document.createElement('div'); card.className = 'label-card'; card.setAttribute('data-id', item.id); card.innerHTML = config.renderCardContent(item); resultsDiv.appendChild(card); }); resultsCard.style.display = 'block'; updateDownloadButtons(); } function handleActionClick(e) { const card = e.target.closest('.label-card'); if (!card) return; const id = card.getAttribute('data-id'); const item = data.find(d => d.id === id); if (!item) return; if (e.target.closest('.btn-view')) { const content = config.renderModalContent(item); showModalWithContent(content); } else if (e.target.closest('.btn-delete')) { if (confirm('Tem certeza que deseja excluir esta etiqueta?')) { data = data.filter(d => d.id !== id); display(); if (config.onDataChange) config.onDataChange(); } } } function updateDownloadButtons() { downloadButtonsDiv.innerHTML = ''; if (data.length === 0) return; const formatKey = outputFormatSelect.value; const format = outputFormats[formatKey]; if (format.type === 'pdf') { const btn = document.createElement('button'); btn.className = 'btn btn-success'; btn.textContent = 'Baixar PDF A4'; btn.onclick = () => downloadPdfA4(data); downloadButtonsDiv.appendChild(btn); } else if (format.type === 'nblb') { const btnPreview = document.createElement('button'); btnPreview.className = 'btn btn-success'; btnPreview.textContent = 'Baixar Pré-visualização'; btnPreview.onclick = () => downloadPdfZebraPreview(data, format); downloadButtonsDiv.appendChild(btnPreview); const btnNblb = document.createElement('button'); btnNblb.className = 'btn btn-secondary'; btnNblb.textContent = 'Baixar p/ Impressora (.nblb)'; btnNblb.onclick = () => downloadNblb(data, format); downloadButtonsDiv.appendChild(btnNblb); } if (config.addExtraButtons) { config.addExtraButtons(downloadButtonsDiv, data); } } function init() { populateOutputSelect(outputFormatSelect); outputFormatSelect.addEventListener('change', updateDownloadButtons); resultsDiv.addEventListener('click', handleActionClick); } return { init, getData, setData, display }; } // --- MÓDULO DO EXCEL --- const excelApp = (function() { const app = createLabelApp({ resultsDivId: 'excelResults', resultsCardId: 'excelResultsCard', outputFormatSelectId: 'excelOutputFormatSelect', downloadButtonsDivId: 'excelDownloadButtons', onDataChange: generateAllBarcodes, renderCardContent: (item) => ` <div class="label-actions"> <button class="action-btn btn-view" title="Visualizar"><svg fill="#007bff" viewBox="0 0 16 16"><path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/><path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/></svg></button> <button class="action-btn btn-delete" title="Excluir"><svg fill="#dc3545" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg></button> </div> <h3>${item.title}</h3> <p>${item.imeis.length} IMEIs</p> <canvas></canvas>`, renderModalContent: (item) => { const content = document.createElement('div'); content.innerHTML = `<h3>${item.title}</h3><p>${item.imeis.length} IMEIs</p>`; const canvas = document.createElement('canvas'); content.appendChild(canvas); const bcid = barcodeTypeSelect.value; let text = (bcid === 'code128') ? (item.imeis[0] || '') : item.barcodeData; bwipjs.toCanvas(canvas, { bcid, text, eclevel: bcid === 'pdf417' ? 2 : 'L' }); return content; }, addExtraButtons: (container, data) => { const btnSummary = document.createElement('button'); btnSummary.className = 'btn btn-success'; btnSummary.textContent = 'Baixar Resumo (.xlsx)'; btnSummary.onclick = () => downloadSummaryExcel(data); container.appendChild(btnSummary); } }); const fileInput = document.getElementById('excelFileInput'); const fileNameSpan = document.getElementById('excelFileName'); const processBtn = document.getElementById('processExcelBtn'); function generateAllBarcodes() { const bcid = barcodeTypeSelect.value; app.getData().forEach(item => { const canvas = document.querySelector(`#excelResults .label-card[data-id="${item.id}"] canvas`); if (canvas) { try { let text = (bcid === 'code128') ? (item.imeis[0] || '') : item.barcodeData; bwipjs.toCanvas(canvas, { bcid, text, eclevel: bcid === 'pdf417' ? 2 : 'L' }); } catch (e) { canvas.outerHTML = `<div style="color:red; font-weight:bold;">Erro: ${e.message}</div>`; } } }); } function handleFileProcess() { const file = fileInput.files[0]; if (!file) return; app.setData([]); app.display(); statusBox.style.display = 'none'; processBtn.disabled = true; processBtn.innerHTML = '<div class="loader"></div>Processando...'; const reader = new FileReader(); reader.onload = (e) => { try { const workbook = XLSX.read(new Uint8Array(e.target.result), { type: 'array' }); const sheet = workbook.Sheets[workbook.SheetNames[0]]; const jsonData = XLSX.utils.sheet_to_json(sheet, { raw: false }); if (!jsonData[0] || !('IMEI' in jsonData[0]) || !('Caixa Master' in jsonData[0])) throw new Error('Colunas "IMEI" e "Caixa Master" não encontradas.'); const groups = {}; jsonData.forEach(row => { const imei = String(row['IMEI'] || '').trim(); const caixaMaster = String(row['Caixa Master'] || '').trim(); const nf = (row['NF'] != null && String(row['NF']).trim() !== '') ? String(row['NF']).trim() : 'S/N'; if (!imei || !caixaMaster) return; const key = `${caixaMaster}_${nf}`; if (!groups[key]) groups[key] = { caixaMaster, nf, imeis: [] }; groups[key].imeis.push(imei); }); const groupedData = Object.values(groups).map((g, index) => ({ id: `excel-${Date.now()}-${index}`, title: `Caixa: ${g.caixaMaster}` + (g.nf !== 'S/N' ? ` / NF: ${g.nf}` : ''), caixaMaster: g.caixaMaster, nf: g.nf, imeis: g.imeis, barcodeData: g.imeis.join('\r\n'), generateBarcode: true })); if (groupedData.length === 0) throw new Error('Nenhum dado válido para agrupar.'); app.setData(groupedData); app.display(); generateAllBarcodes(); statusBox.className = 'status-box success'; statusBox.textContent = `Sucesso! ${groupedData.length} etiquetas geradas.`; } catch (error) { statusBox.className = 'status-box error'; statusBox.textContent = `Erro: ${error.message}`; } finally { statusBox.style.display = 'block'; processBtn.disabled = false; processBtn.innerHTML = 'Processar Excel'; } }; reader.readAsArrayBuffer(file); } function init() { app.init(); fileInput.addEventListener('change', (e) => { fileNameSpan.textContent = e.target.files.length > 0 ? e.target.files[0].name : 'Nenhum arquivo selecionado'; processBtn.disabled = e.target.files.length === 0; if(e.target.files.length > 0) handleFileProcess(); e.target.value = null; // Permite re-upload do mesmo arquivo }); processBtn.addEventListener('click', handleFileProcess); } return { init, getData: app.getData, generateAllBarcodes }; })(); // --- MÓDULO DO XML --- const xmlApp = (function() { const app = createLabelApp({ resultsDivId: 'xmlResults', resultsCardId: 'xmlResultsCard', outputFormatSelectId: 'xmlOutputFormatSelect', downloadButtonsDivId: 'xmlDownloadButtons', renderCardContent: (item) => ` <div class="label-actions"> <button class="action-btn btn-view" title="Visualizar"><svg fill="#007bff" viewBox="0 0 16 16"><path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/><path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/></svg></button> <button class="action-btn btn-delete" title="Excluir"><svg fill="#dc3545" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg></button> </div> <pre>${item.barcodeData}</pre>`, renderModalContent: (item) => { const content = document.createElement('pre'); content.textContent = item.barcodeData; return content; } }); const fileInput = document.getElementById('xmlFileInput'); const fileNameSpan = document.getElementById('xmlFileName'); async function handleXmlFiles(event) { app.setData([]); const files = event.target.files; if (files.length === 0) { fileNameSpan.textContent = 'Nenhum arquivo selecionado'; app.display(); return; } fileNameSpan.textContent = `${files.length} arquivo(s) selecionado(s)`; statusBox.style.display = 'block'; statusBox.className = 'status-box info'; statusBox.innerHTML = '<div class="loader"></div>Processando arquivos XML...'; const promises = Array.from(files).map(file => file.text()); const contents = await Promise.all(promises); let parseErrors = 0; let parsedData = []; contents.forEach((xmlString, index) => { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, "application/xml"); if (xmlDoc.getElementsByTagName("parsererror").length) { throw new Error(`Arquivo "${files[index].name}" não parece ser um XML válido.`); } const get = (s) => xmlDoc.querySelector(s)?.textContent || ''; const nNF = get('ide > nNF'); const barcodeData = [ `Nota fiscal: ${nNF}`, `\nDestinatário:\n${get('dest > xNome')}`, `\nCNPJ/CPF: ${get('dest > CPF') || get('dest > CNPJ')}`, `\nEndereço: ${get('enderDest > xLgr')}, ${get('enderDest > nro')}\n${get('enderDest > CEP')} - ${get('enderDest > xMun')} - ${get('enderDest > UF')}`, `\nTransportador:\n${get('transporta > xNome')}` ].join('\n\n'); parsedData.push({ id: `xml-${Date.now()}-${index}`, title: `NF: ${nNF}`, barcodeData, generateBarcode: false }); } catch(e) { console.error("Erro no XML", e); parseErrors++; } }); statusBox.className = parseErrors > 0 ? 'status-box error' : 'status-box success'; statusBox.textContent = `Sucesso! ${parsedData.length} etiquetas geradas. ${parseErrors > 0 ? `${parseErrors} arquivo(s) com erro.` : ''}`; app.setData(parsedData); app.display(); event.target.value = null; // Permite re-upload } function init() { app.init(); fileInput.addEventListener('change', handleXmlFiles); } return { init, getData: app.getData }; })(); // --- MÓDULO MANUAL --- const manualApp = (function() { const app = createLabelApp({ resultsDivId: 'manualResults', resultsCardId: 'manualResultsCard', outputFormatSelectId: 'manualOutputFormatSelect', downloadButtonsDivId: 'manualDownloadButtons', onDataChange: generateManualBarcode, renderCardContent: (item) => { let contentHTML = ` <div class="label-actions"> <button class="action-btn btn-view" title="Visualizar"><svg fill="#007bff" viewBox="0 0 16 16"><path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/><path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/></svg></button> <button class="action-btn btn-delete" title="Excluir"><svg fill="#dc3545" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg></button> </div>`; if (item.generateBarcode) { if (item.title) contentHTML += `<h3>${item.title}</h3>`; contentHTML += `<canvas></canvas>`; } else { contentHTML += `<pre>${item.barcodeData}</pre>`; } return contentHTML; }, renderModalContent: (item) => { const content = document.createElement('div'); if (item.generateBarcode) { if(item.title) content.innerHTML = `<h3>${item.title}</h3>`; const canvas = document.createElement('canvas'); content.appendChild(canvas); const bcid = barcodeTypeSelect.value; let text = (bcid === 'code128') ? item.barcodeData.split('\n')[0].split('\r')[0] : item.barcodeData; bwipjs.toCanvas(canvas, { bcid, text, eclevel: bcid === 'pdf417' ? 2 : 'L' }); } else { content.innerHTML = `<pre>${item.barcodeData}</pre>`; } return content; } }); const manualInput = document.getElementById('manualInput'); const manualOutputType = document.getElementById('manualOutputType'); function generateManualBarcode() { const item = app.getData()[0]; if (!item || !item.generateBarcode) return; const canvas = document.querySelector(`#manualResults .label-card[data-id="${item.id}"] canvas`); if(canvas) { try { const bcid = barcodeTypeSelect.value; let text = (bcid === 'code128') ? item.barcodeData.split('\n')[0].split('\r')[0] : item.barcodeData; bwipjs.toCanvas(canvas, { bcid, text, eclevel: bcid === 'pdf417' ? 2 : 'L' }); } catch (e) { canvas.outerHTML = `<div style="color:red; font-weight:bold;">Erro: ${e.message}</div>`; } } } function handleManualText() { const text = manualInput.value.trim(); if(!text) { app.setData([]); app.display(); return; }; const type = manualOutputType.value; const generateCode = type === 'text_and_code' || type === 'code_only'; const showText = type === 'text_and_code' || type === 'text_only'; const title = showText && generateCode ? text.split('\n')[0].split('\r')[0] :''; app.setData([{ id: `manual-${Date.now()}`, title: title, barcodeData: text, generateBarcode: generateCode }]); app.display(); if (generateCode) generateManualBarcode(); } function init() { app.init(); document.getElementById('processManualBtn').addEventListener('click', handleManualText); } return { init, getData: app.getData, generateManualBarcode }; })(); // --- GERENCIAMENTO DE ABAS E EVENTOS GLOBAIS --- const tabLinks = document.querySelectorAll('.tab-link'); const tabContents = document.querySelectorAll('.tab-content'); const resultCards = document.querySelectorAll('.card[id$="ResultsCard"]'); tabLinks.forEach(link => { link.addEventListener('click', () => { const activeTab = link.getAttribute('data-tab'); tabLinks.forEach(item => item.classList.remove('active')); tabContents.forEach(item => item.classList.remove('active')); link.classList.add('active'); document.getElementById(activeTab + 'Tab').classList.add('active'); resultCards.forEach(card => card.style.display = 'none'); statusBox.style.display = 'none'; const appDataMap = { excel: excelApp.getData(), xml: xmlApp.getData(), manual: manualApp.getData() }; if (appDataMap[activeTab] && appDataMap[activeTab].length > 0) { document.getElementById(activeTab + 'ResultsCard').style.display = 'block'; } }); }); modalClose.onclick = () => { modalView.style.display = 'none'; }; modalView.onclick = (e) => { if (e.target === modalView) modalView.style.display = 'none'; }; settingsTriggerBtn.onclick = () => { settingsModal.style.display = 'flex'; }; settingsModalClose.onclick = () => { settingsModal.style.display = 'none'; }; settingsModal.onclick = (e) => { if (e.target === settingsModal) settingsModal.style.display = 'none'; }; barcodeTypeSelect.addEventListener('change', () => { if (excelApp.getData().length > 0) { excelApp.generateAllBarcodes(); } if(manualApp.getData().length > 0){ manualApp.generateManualBarcode(); } }); // --- INICIALIZAÇÃO DOS MÓDULOS --- excelApp.init(); xmlApp.init(); manualApp.init(); }); </script>
Você tinha “quebras” e espaços residuais entrando no conteúdo do código de barras (principalmente por \r\n
e por sufixos/linhas vazias no fim). O efeito visual é esse “espaço” no final quando você lê a etiqueta.
Corrigi de forma robusta:
Criei normalizeMultiline()
para:
\n
;Passei a usar essa normalização:
bwipjs
(PDF417/QR/Code128);^FH
e codifiquei \n
como _0A
para PDF417, evitando qualquer caractere invisível extra.Para Code128 continuei usando só a primeira linha, já sem espaços finais.
Abaixo está o código completo com as correções aplicadas.
html<!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Gerador de Etiquetas (Versão Profissional)</title> <style> :root { --primary-color: #007bff; --secondary-color: #6c757d; --success-color: #28a745; --danger-color: #dc3545; --light-bg-color: #f8f9fa; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --border-radius: 0.25rem; --card-shadow: 0 4px 8px rgba(0,0,0,0.05); } body { font-family: var(--font-family); background-color: #f4f7f9; color: #343a40; margin: 0; padding: 1.5rem; display: flex; justify-content: center; } .container { width: 100%; max-width: 1200px; } h1, h2 { color: var(--primary-color); text-align: center; } h1 { margin-bottom: 2rem; } h2 { margin-bottom: 1.5rem; text-align: left; border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem;} .card { background-color: #fff; border: 1px solid #dee2e6; border-radius: var(--border-radius); padding: 2rem; margin-bottom: 1.5rem; box-shadow: var(--card-shadow); } .tab-container { display: flex; border-bottom: 1px solid #dee2e6; margin-bottom: 1.5rem; } .tab-link { padding: 0.75rem 1.25rem; cursor: pointer; border: none; background-color: transparent; font-size: 1rem; font-weight: 500; color: var(--secondary-color); border-bottom: 3px solid transparent; transition: all 0.2s ease; } .tab-link:hover { color: var(--primary-color); } .tab-link.active { color: var(--primary-color); border-bottom-color: var(--primary-color); } .tab-content { display: none; } .tab-content.active { display: block; } .btn { display: inline-flex; align-items: center; gap: 0.5rem; font-weight: 500; color: #fff; text-align: center; cursor: pointer; user-select: none; background-color: var(--primary-color); border: 1px solid var(--primary-color); padding: 0.6rem 1.2rem; font-size: 1rem; border-radius: var(--border-radius); transition: all 0.15s ease-in-out; text-decoration: none; } .btn:hover { opacity: 0.85; } .btn-secondary { background-color: var(--secondary-color); border-color: var(--secondary-color); } .btn-success { background-color: var(--success-color); border-color: var(--success-color); } .file-upload-wrapper { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; } input[type="file"] { display: none; } .file-upload-label { border: 1px solid var(--primary-color); color: var(--primary-color); padding: 0.6rem 1.2rem; border-radius: var(--border-radius); cursor: pointer; transition: all 0.2s ease;} .file-upload-label:hover { background-color: var(--primary-color); color: #fff; } .file-name { font-style: italic; color: var(--secondary-color); } textarea.input-area { width: 100%; min-height: 150px; padding: 0.75rem; font-family: "Courier New", Courier, monospace; font-size: 1rem; border-radius: var(--border-radius); border: 1px solid #ccc; box-sizing: border-box; } select { padding: 0.6rem; border-radius: var(--border-radius); border: 1px solid #ccc; font-size: 1rem; } .status-box { margin-top: 1.5rem; padding: 1rem; border-radius: var(--border-radius); display: none; word-wrap: break-word; } .status-box.success { border: 1px solid #c3e6cb; background-color: #d4edda; color: #155724; } .status-box.error { border: 1px solid #f5c6cb; background-color: #f8d7da; color: #721c24; } .status-box.info { border: 1px solid #bde5f8; background-color: #cce5ff; color: #004085; } .loader { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary-color); border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-right: 0.75rem; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .label-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1.5rem; } .label-card { border: 1px solid #ccc; border-radius: var(--border-radius); padding: 1rem; background: #fff; display: flex; flex-direction: column; align-items: center; min-height: 150px; position: relative; padding-top: 2.5rem; } .label-card h3 { margin-top: 0; font-size: 1.1rem; word-break: break-all; text-align: center; } .label-card p { margin: 0.25rem 0; font-size: 0.9rem; } .label-card canvas { max-width: 100%; height: auto; margin-top: 1rem; border: 1px solid #eee; } .label-card pre { white-space: pre-wrap; word-wrap: break-word; font-family: var(--font-family); font-size: 0.95rem; margin: 0; background-color: var(--light-bg-color); padding: 1rem; border-radius: var(--border-radius); border: 1px solid #eee; width: 100%; box-sizing: border-box; } .results-controls { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #dee2e6; margin-bottom: 1.5rem; } .download-buttons { display: flex; gap: 1rem; flex-wrap: wrap; } .label-actions { position: absolute; top: 0.5rem; right: 0.5rem; display: flex; gap: 0.5rem; background-color: rgba(255,255,255,0.8); border-radius: 5px; padding: 2px; } .action-btn { background: none; border: none; cursor: pointer; padding: 0.3rem; opacity: 0.6; transition: opacity 0.2s ease; } .action-btn:hover { opacity: 1; } .action-btn svg { width: 18px; height: 18px; display: block; } .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); display: none; justify-content: center; align-items: center; z-index: 1000; } .modal-content { background: #fff; padding: 2rem; border-radius: var(--border-radius); max-width: 90%; width: 500px; max-height: 90%; overflow-y: auto; text-align: center; position: relative; } .modal-close { position: absolute; top: 0.5rem; right: 1rem; font-size: 2rem; font-weight: bold; cursor: pointer; color: #555; } .modal-body { margin-top: 1rem; } .modal-body canvas { max-width: 100%; } .modal-body pre { text-align: left; } </style> </head> <body> <div class="container"> <div style="display: flex; justify-content: center; align-items: center; position: relative;"> <h1>Gerador de Etiquetas</h1> <button id="settings-trigger-btn" aria-label="Abrir Configurações de Impressão" title="Configurações de Impressão" style="position: absolute; right: 0; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; padding: 0.5rem; opacity: 0.7; transition: opacity 0.2s;"> <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#6c757d"> <title>Ícone de Configurações</title> <g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M4 12H20M4 6H20M4 18H20" stroke="#6c757d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g> </svg> </button> </div> <div class="card"> <h2>1. Entrada de Dados</h2> <div class="tab-container"> <button class="tab-link active" data-tab="excel">Arquivo Excel</button> <button class="tab-link" data-tab="xml">Arquivo XML</button> <button class="tab-link" data-tab="manual">Digitação Manual</button> </div> <div id="excelTab" class="tab-content active"> <p>Selecione um arquivo Excel (.xlsx).</p> <div class="file-upload-wrapper"> <label for="excelFileInput" class="file-upload-label">Escolher Arquivo</label> <input type="file" id="excelFileInput" accept=".xlsx,.xls"> <span id="excelFileName" class="file-name">Nenhum arquivo selecionado</span> </div> <div style="margin-top: 1rem;"> <button id="processExcelBtn" class="btn" disabled>Processar Excel</button> </div> </div> <div id="xmlTab" class="tab-content"> <p>Selecione um ou mais arquivos XML de NF-e.</p> <div class="file-upload-wrapper"> <label for="xmlFileInput" class="file-upload-label">Escolher Arquivos</label> <input type="file" id="xmlFileInput" accept=".xml, text/xml" multiple> <span id="xmlFileName" class="file-name">Nenhum arquivo selecionado</span> </div> </div> <div id="manualTab" class="tab-content"> <p>Digite ou cole o conteúdo que deseja na etiqueta.</p> <textarea id="manualInput" class="input-area" placeholder="Nota fiscal: ..."></textarea> <div style="margin-top: 1rem; display: flex; align-items: center; gap: 1rem;"> <button id="processManualBtn" class="btn">Gerar Etiqueta</button> <select id="manualOutputType"> <option value="text_only" selected>Somente Escrita</option> <option value="text_and_code">Escrita e Código</option> <option value="code_only">Somente Código</option> </select> </div> </div> <div id="statusBox" class="status-box"></div> </div> <div id="excelResultsCard" class="card" style="display: none;"> <h2>Resultados do Excel</h2> <div class="results-controls"> <div class="output-selection-wrapper"> <label for="excelOutputFormatSelect"><strong>Tipo de Saída:</strong></label> <select id="excelOutputFormatSelect"></select> </div> <div id="excelDownloadButtons" class="download-buttons"></div> </div> <div id="excelResults" class="label-grid"></div> </div> <div id="xmlResultsCard" class="card" style="display: none;"> <h2>Resultados do XML</h2> <div class="results-controls"> <div class="output-selection-wrapper"> <label for="xmlOutputFormatSelect"><strong>Tipo de Saída:</strong></label> <select id="xmlOutputFormatSelect"></select> </div> <div id="xmlDownloadButtons" class="download-buttons"></div> </div> <div id="xmlResults" class="label-grid"></div> </div> <div id="manualResultsCard" class="card" style="display: none;"> <h2>Resultados da Digitação Manual</h2> <div class="results-controls"> <div class="output-selection-wrapper"> <label for="manualOutputFormatSelect"><strong>Tipo de Saída:</strong></label> <select id="manualOutputFormatSelect"></select> </div> <div id="manualDownloadButtons" class="download-buttons"></div> </div> <div id="manualResults" class="label-grid"></div> </div> </div> <div id="modal-view" class="modal-overlay"> <div class="modal-content"> <span class="modal-close">×</span> <div id="modal-body" class="modal-body"></div> </div> </div> <div id="settings-modal" class="modal-overlay"> <div class="modal-content"> <span id="settings-modal-close" class="modal-close">×</span> <h2>Configurações de Impressão</h2> <div class="settings-body" style="margin-top: 1.5rem; text-align: left;"> <label for="barcodeTypeSelect"><strong>Tipo de Código de Barras (Excel e Manual):</strong></label> <select id="barcodeTypeSelect" style="width: 100%; margin-top: 0.5rem;"> <option value="pdf417">PDF417</option> <option value="code128">Code 128</option> <option value="qrcode">QR Code</option> </select> <p style="font-size: 0.8rem; color: #6c757d; margin-top: 1rem;"> Atenção: Code 128 não suporta múltiplas linhas. Apenas a primeira linha de dados será usada. </p> </div> </div> </div> <script src="https://cdn.sheetjs.com/xlsx-0.19.3/package/dist/xlsx.full.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/bwip-js/3.2.1/bwip-js.min.js"></script> <script> document.addEventListener('DOMContentLoaded', function () { // Verifica se as bibliotecas essenciais foram carregadas if (typeof XLSX === 'undefined' || typeof jspdf === 'undefined' || typeof bwipjs === 'undefined') { document.getElementById('statusBox').innerHTML = '<strong>Erro Crítico:</strong> Bibliotecas externas não foram carregadas. Verifique sua conexão com a internet.'; return; } // --- ELEMENTOS GLOBAIS --- const statusBox = document.getElementById('statusBox'); const modalView = document.getElementById('modal-view'); const modalBody = document.getElementById('modal-body'); const modalClose = document.querySelector('#modal-view .modal-close'); const settingsTriggerBtn = document.getElementById('settings-trigger-btn'); const settingsModal = document.getElementById('settings-modal'); const settingsModalClose = document.getElementById('settings-modal-close'); const barcodeTypeSelect = document.getElementById('barcodeTypeSelect'); // --- CONFIGURAÇÕES E FUNÇÕES GLOBAIS DE DOWNLOAD --- const outputFormats = { 'pdf-a4': { name: 'Folha A4 (Layout em Lista)', type: 'pdf' }, 'zpl-100x150': { name: 'Zebra (100 x 150mm)', type: 'nblb', width: 100, height: 150 }, 'zpl-100x70': { name: 'Zebra (100 x 70mm)', type: 'nblb', width: 100, height: 70 }, 'zpl-100x50': { name: 'Zebra (100 x 50mm)', type: 'nblb', width: 100, height: 50 }, 'zpl-70x50': { name: 'Zebra (70 x 50mm)', type: 'nblb', width: 70, height: 50 }, 'zpl-70x30': { name: 'Zebra (70 x 30mm)', type: 'nblb', width: 70, height: 30 } }; function populateOutputSelect(selectElement) { selectElement.innerHTML = ''; const pdfGroup = document.createElement('optgroup'); pdfGroup.label = 'PDF Padrão'; const zplGroup = document.createElement('optgroup'); zplGroup.label = 'Impressora Zebra'; for (const key in outputFormats) { const format = outputFormats[key]; const option = document.createElement('option'); option.value = key; option.textContent = format.name; if (format.type === 'pdf') pdfGroup.appendChild(option); else zplGroup.appendChild(option); } selectElement.appendChild(pdfGroup); selectElement.appendChild(zplGroup); } function showModalWithContent(contentNode) { modalBody.innerHTML = ''; modalBody.appendChild(contentNode); modalView.style.display = 'flex'; } // ============ NORMALIZAÇÃO (correção do “espaço” no final) ============ function normalizeMultiline(str) { // força string str = String(str ?? ''); // 1) unifica CRLF/CR para LF str = str.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); // 2) remove espaços/tabs à direita de cada linha str = str.replace(/[ \t]+$/gm, ''); // 3) remove quebras/linhas vazias finais str = str.replace(/\n+$/g, ''); return str; } // ====================================================================== // --- Funções de Download --- function downloadPdfA4(data) { if (data.length === 0) return; const { jsPDF } = window.jspdf; const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' }); const pageMargin = 15; let currentY = pageMargin; data.forEach((item) => { const bcid = barcodeTypeSelect.value; const labelHeight = bcid === 'pdf417' ? 40 : 50; if (currentY + labelHeight > pdf.internal.pageSize.getHeight() - pageMargin) { pdf.addPage(); currentY = pageMargin; } if(item.title) pdf.setFontSize(9).text(item.title, pageMargin, currentY + 5); if (item.generateBarcode) { const canvas = document.createElement('canvas'); let text = normalizeMultiline(item.barcodeData); if(bcid === 'code128') text = text.split('\n')[0]; // só 1ª linha, sem espaços finais bwipjs.toCanvas(canvas, { bcid, text, eclevel: bcid === 'pdf417' ? 2 : 'L' }); const imgHeight = bcid === 'pdf417' ? 25 : (bcid === 'qrcode' ? 35 : 20); const imgWidth = bcid === 'pdf417' ? 80 : (bcid === 'qrcode' ? 35 : 80); pdf.addImage(canvas.toDataURL('image/png'), 'PNG', pageMargin, currentY + 8, imgWidth, imgHeight); } else if (item.barcodeData) { const maxWidth = pdf.internal.pageSize.getWidth() - (pageMargin * 2); const maxHeight = labelHeight - 10; let fontSize = 9; let dimensions; const printed = normalizeMultiline(item.barcodeData); do { pdf.setFontSize(fontSize); dimensions = pdf.getTextDimensions(printed, { maxWidth: maxWidth }); if (dimensions.h > maxHeight && fontSize > 4) { fontSize -= 0.5; } } while (dimensions.h > maxHeight && fontSize > 4); pdf.setFontSize(fontSize); pdf.text(printed, pageMargin, currentY + (item.title ? 12 : 5), { maxWidth: maxWidth }); } pdf.line(pageMargin, currentY + labelHeight - 2, pdf.internal.pageSize.getWidth() - pageMargin, currentY + labelHeight - 2); currentY += labelHeight; }); pdf.save(`etiquetas_A4_${new Date().toISOString().slice(0,10)}.pdf`); } function downloadPdfZebraPreview(data, format) { if (data.length === 0) return; const { jsPDF } = window.jspdf; const orientation = format.width > format.height ? 'l' : 'p'; const pdf = new jsPDF({ orientation, unit: 'mm', format: [format.width, format.height] }); data.forEach((item, index) => { if (index > 0) pdf.addPage([format.width, format.height], orientation); const margin = 3; if(item.generateBarcode) { const canvas = document.createElement('canvas'); const bcid = barcodeTypeSelect.value; let text = normalizeMultiline(item.barcodeData); if(bcid === 'code128') text = text.split('\n')[0]; bwipjs.toCanvas(canvas, { bcid, text, eclevel: bcid === 'pdf417' ? 2 : 'L' }); const titleHeight = item.title ? 8 : 0; const availableWidth = format.width - (margin * 2); const availableHeight = format.height - titleHeight - (margin * 2); let finalWidth = availableWidth; let finalHeight = (canvas.height * finalWidth) / canvas.width; if (finalHeight > availableHeight) { finalHeight = availableHeight; finalWidth = finalHeight * canvas.width / canvas.height; } const totalContentHeight = titleHeight + finalHeight; const startY = (format.height - totalContentHeight) / 2; const xPos = margin + (availableWidth - finalWidth) / 2; if(item.title) pdf.setFontSize(9).text(item.title, format.width / 2, startY + 4, { align: 'center' }); pdf.addImage(canvas.toDataURL('image/png'), 'PNG', xPos, startY + titleHeight, finalWidth, finalHeight); } else if (item.barcodeData) { const maxWidth = format.width - (margin * 2); const maxHeight = format.height - (margin * 2); let fontSize = 9; let dimensions; const printed = normalizeMultiline(item.barcodeData); do { pdf.setFontSize(fontSize); dimensions = pdf.getTextDimensions(printed, { maxWidth: maxWidth }); if (dimensions.h > maxHeight && fontSize > 4) { fontSize -= 0.5; } } while (dimensions.h > maxHeight && fontSize > 4); const startY = (format.height - dimensions.h) / 2 + margin; pdf.setFontSize(fontSize); pdf.text(printed, margin, startY, { maxWidth: maxWidth }); } }); pdf.save(`preview_${format.width}x${format.height}.pdf`); } function downloadNblb(data, format) { if (data.length === 0) return; const dotsPerMm = 8; const labelWidthDots = Math.round(format.width * dotsPerMm); const marginDots = 24; let zplCommands = ""; const bcid = barcodeTypeSelect.value; data.forEach(item => { const cleanTitle = item.title ? item.title.replace(/[\^~]/g, '') : 'Etiqueta'; const raw = normalizeMultiline(item.barcodeData).replace(/[\^~]/g, ''); zplCommands += `^XA\n^CI28\n`; if (item.generateBarcode) { if(item.title) zplCommands += `^FO${marginDots},${marginDots}^A0N,28,28^FD${cleanTitle}^FS\n`; const yPos = item.title ? marginDots + 40 : marginDots; if (bcid === 'qrcode') { zplCommands += `^FO${marginDots + 20},${yPos}^BQN,2,5^FDQA,${raw}^FS\n`; } else if (bcid === 'code128') { const firstLine = raw.split('\n')[0]; zplCommands += `^BY2,3,60^FO${marginDots},${yPos}^BCN,,N,N^FD${firstLine}^FS\n`; } else { // pdf417 // Usa ^FH e codifica \n como _0A para garantir múltiplas linhas sem sobras const encoded = raw.replace(/\n/g, '_0A'); zplCommands += `^FO${marginDots},${yPos}^B7N,5,3,,,N^FH^FD${encoded}^FS\n`; } } else { const printed = normalizeMultiline(raw); zplCommands += `^FO${marginDots},${marginDots}^FB${labelWidthDots - (marginDots * 2)},20,0,L,0^A0N,24,24^FD${printed}^FS\n`; } zplCommands += `^XZ\n\n`; }); const blob = new Blob([zplCommands], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `etiquetas_${format.width}x${format.height}mm_${new Date().toISOString().slice(0,10)}.nblb`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function downloadSummaryExcel(data) { const exportData = data.map(group => ({ 'Caixa Master': group.caixaMaster, 'NF': group.nf === 'S/N' ? '' : group.nf, 'Quantidade de IMEIs': group.imeis.length, 'Lista de IMEIs': group.imeis.join(';') })); const worksheet = XLSX.utils.json_to_sheet(exportData); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "Resumo Agrupado"); XLSX.writeFile(workbook, `resumo_etiquetas_${new Date().toISOString().slice(0,10)}.xlsx`); } // --- FIM Funções de Download --- // --- FÁBRICA DE APLICAÇÃO DE ETIQUETAS (LÓGICA REUTILIZÁVEL) --- function createLabelApp(config) { let data = []; const resultsDiv = document.getElementById(config.resultsDivId); const resultsCard = document.getElementById(config.resultsCardId); const outputFormatSelect = document.getElementById(config.outputFormatSelectId); const downloadButtonsDiv = document.getElementById(config.downloadButtonsDivId); function getData() { return data; } function setData(newData) { data = newData; } function display() { resultsDiv.innerHTML = ''; if (data.length === 0) { resultsCard.style.display = 'none'; return; } data.forEach(item => { const card = document.createElement('div'); card.className = 'label-card'; card.setAttribute('data-id', item.id); card.innerHTML = config.renderCardContent(item); resultsDiv.appendChild(card); }); resultsCard.style.display = 'block'; updateDownloadButtons(); } function handleActionClick(e) { const card = e.target.closest('.label-card'); if (!card) return; const id = card.getAttribute('data-id'); const item = data.find(d => d.id === id); if (!item) return; if (e.target.closest('.btn-view')) { const content = config.renderModalContent(item); showModalWithContent(content); } else if (e.target.closest('.btn-delete')) { if (confirm('Tem certeza que deseja excluir esta etiqueta?')) { data = data.filter(d => d.id !== id); display(); if (config.onDataChange) config.onDataChange(); } } } function updateDownloadButtons() { downloadButtonsDiv.innerHTML = ''; if (data.length === 0) return; const formatKey = outputFormatSelect.value; const format = outputFormats[formatKey]; if (format.type === 'pdf') { const btn = document.createElement('button'); btn.className = 'btn btn-success'; btn.textContent = 'Baixar PDF A4'; btn.onclick = () => downloadPdfA4(data); downloadButtonsDiv.appendChild(btn); } else if (format.type === 'nblb') { const btnPreview = document.createElement('button'); btnPreview.className = 'btn btn-success'; btnPreview.textContent = 'Baixar Pré-visualização'; btnPreview.onclick = () => downloadPdfZebraPreview(data, format); downloadButtonsDiv.appendChild(btnPreview); const btnNblb = document.createElement('button'); btnNblb.className = 'btn btn-secondary'; btnNblb.textContent = 'Baixar p/ Impressora (.nblb)'; btnNblb.onclick = () => downloadNblb(data, format); downloadButtonsDiv.appendChild(btnNblb); } if (config.addExtraButtons) { config.addExtraButtons(downloadButtonsDiv, data); } } function init() { populateOutputSelect(outputFormatSelect); outputFormatSelect.addEventListener('change', updateDownloadButtons); resultsDiv.addEventListener('click', handleActionClick); } return { init, getData, setData, display }; } // --- MÓDULO DO EXCEL --- const excelApp = (function() { const app = createLabelApp({ resultsDivId: 'excelResults', resultsCardId: 'excelResultsCard', outputFormatSelectId: 'excelOutputFormatSelect', downloadButtonsDivId: 'excelDownloadButtons', onDataChange: generateAllBarcodes, renderCardContent: (item) => ` <div class="label-actions"> <button class="action-btn btn-view" title="Visualizar"><svg fill="#007bff" viewBox="0 0 16 16"><path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/><path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/></svg></button> <button class="action-btn btn-delete" title="Excluir"><svg fill="#dc3545" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg></button> </div> <h3>${item.title}</h3> <p>${item.imeis.length} IMEIs</p> <canvas></canvas>`, renderModalContent: (item) => { const content = document.createElement('div'); content.innerHTML = ``; const canvas = document.createElement('canvas'); content.appendChild(canvas); const bcid = barcodeTypeSelect.value; let text = normalizeMultiline(bcid === 'code128' ? (item.imeis[0] || '') : item.barcodeData); if (bcid === 'code128') text = text.split('\n')[0]; bwipjs.toCanvas(canvas, { bcid, text, eclevel: bcid === 'pdf417' ? 2 : 'L' }); return content; }, addExtraButtons: (container, data) => { const btnSummary = document.createElement('button'); btnSummary.className = 'btn btn-success'; btnSummary.textContent = 'Baixar Resumo (.xlsx)'; btnSummary.onclick = () => downloadSummaryExcel(data); container.appendChild(btnSummary); } }); const fileInput = document.getElementById('excelFileInput'); const fileNameSpan = document.getElementById('excelFileName'); const processBtn = document.getElementById('processExcelBtn'); function generateAllBarcodes() { const bcid = barcodeTypeSelect.value; app.getData().forEach(item => { const canvas = document.querySelector(`#excelResults .label-card[data-id="${item.id}"] canvas`); if (canvas) { try { let text = normalizeMultiline(bcid === 'code128' ? (item.imeis[0] || '') : item.barcodeData); if (bcid === 'code128') text = text.split('\n')[0]; bwipjs.toCanvas(canvas, { bcid, text, eclevel: bcid === 'pdf417' ? 2 : 'L' }); } catch (e) { canvas.outerHTML = ``; } } }); } function handleFileProcess() { const file = fileInput.files[0]; if (!file) return; app.setData([]); app.display(); statusBox.style.display = 'none'; processBtn.disabled = true; processBtn.innerHTML = '<div class="loader"></div>Processando...'; const reader = new FileReader(); reader.onload = (e) => { try { const workbook = XLSX.read(new Uint8Array(e.target.result), { type: 'array' }); const sheet = workbook.Sheets[workbook.SheetNames[0]]; const jsonData = XLSX.utils.sheet_to_json(sheet, { raw: false }); if (!jsonData[0] || !('IMEI' in jsonData[0]) || !('Caixa Master' in jsonData[0])) throw new Error('Colunas "IMEI" e "Caixa Master" não encontradas.'); const groups = {}; jsonData.forEach(row => { const imei = String(row['IMEI'] || '').trim(); const caixaMaster = String(row['Caixa Master'] || '').trim(); const nf = (row['NF'] != null && String(row['NF']).trim() !== '') ? String(row['NF']).trim() : 'S/N'; if (!imei || !caixaMaster) return; const key = `${caixaMaster}_${nf}`; if (!groups[key]) groups[key] = { caixaMaster, nf, imeis: [] }; groups[key].imeis.push(imei); }); const groupedData = Object.values(groups).map((g, index) => { const joined = normalizeMultiline(g.imeis.join('\n')); // linhas sem CR/LF residual e sem fim vazio return { id: `excel-${Date.now()}-${index}`, title: `Caixa: ${g.caixaMaster}` + (g.nf !== 'S/N' ? ` / NF: ${g.nf}` : ''), caixaMaster: g.caixaMaster, nf: g.nf, imeis: g.imeis, barcodeData: joined, generateBarcode: true }; }); if (groupedData.length === 0) throw new Error('Nenhum dado válido para agrupar.'); app.setData(groupedData); app.display(); generateAllBarcodes(); statusBox.className = 'status-box success'; statusBox.textContent = `Sucesso! ${groupedData.length} etiquetas geradas.`; } catch (error) { statusBox.className = 'status-box error'; statusBox.textContent = `Erro: ${error.message}`; } finally { statusBox.style.display = 'block'; processBtn.disabled = false; processBtn.innerHTML = 'Processar Excel'; } }; reader.readAsArrayBuffer(file); } function init() { app.init(); fileInput.addEventListener('change', (e) => { fileNameSpan.textContent = e.target.files.length > 0 ? e.target.files[0].name : 'Nenhum arquivo selecionado'; processBtn.disabled = e.target.files.length === 0; if(e.target.files.length > 0) handleFileProcess(); e.target.value = null; // Permite re-upload do mesmo arquivo }); processBtn.addEventListener('click', handleFileProcess); } return { init, getData: app.getData, generateAllBarcodes }; })(); // --- MÓDULO DO XML --- const xmlApp = (function() { const app = createLabelApp({ resultsDivId: 'xmlResults', resultsCardId: 'xmlResultsCard', outputFormatSelectId: 'xmlOutputFormatSelect', downloadButtonsDivId: 'xmlDownloadButtons', renderCardContent: (item) => ` <div class="label-actions"> <button class="action-btn btn-view" title="Visualizar"><svg fill="#007bff" viewBox="0 0 16 16"><path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/><path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/></svg></button> <button class="action-btn btn-delete" title="Excluir"><svg fill="#dc3545" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg></button> </div> <pre>${item.barcodeData}</pre>`, renderModalContent: (item) => { const content = document.createElement('pre'); content.textContent = item.barcodeData; return content; } }); const fileInput = document.getElementById('xmlFileInput'); const fileNameSpan = document.getElementById('xmlFileName'); async function handleXmlFiles(event) { app.setData([]); const files = event.target.files; if (files.length === 0) { fileNameSpan.textContent = 'Nenhum arquivo selecionado'; app.display(); return; } fileNameSpan.textContent = `${files.length} arquivo(s) selecionado(s)`; statusBox.style.display = 'block'; statusBox.className = 'status-box info'; statusBox.innerHTML = '<div class="loader"></div>Processando arquivos XML...'; const promises = Array.from(files).map(file => file.text()); const contents = await Promise.all(promises); let parseErrors = 0; let parsedData = []; contents.forEach((xmlString, index) => { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, "application/xml"); if (xmlDoc.getElementsByTagName("parsererror").length) { throw new Error(`Arquivo "${files[index].name}" não parece ser um XML válido.`); } const get = (s) => xmlDoc.querySelector(s)?.textContent || ''; const nNF = get('ide > nNF'); const barcodeData = normalizeMultiline([ `Nota fiscal: ${nNF}`, `\nDestinatário:\n${get('dest > xNome')}`, `\nCNPJ/CPF: ${get('dest > CPF') || get('dest > CNPJ')}`, `\nEndereço: ${get('enderDest > xLgr')}, ${get('enderDest > nro')}\n${get('enderDest > CEP')} - ${get('enderDest > xMun')} - ${get('enderDest > UF')}`, `\nTransportador:\n${get('transporta > xNome')}` ].join('\n\n')); parsedData.push({ id: `xml-${Date.now()}-${index}`, title: `NF: ${nNF}`, barcodeData, generateBarcode: false }); } catch(e) { console.error("Erro no XML", e); parseErrors++; } }); statusBox.className = parseErrors > 0 ? 'status-box error' : 'status-box success'; statusBox.textContent = `Sucesso! ${parsedData.length} etiquetas geradas. ${parseErrors > 0 ? `${parseErrors} arquivo(s) com erro.` : ''}`; app.setData(parsedData); app.display(); event.target.value = null; // Permite re-upload } function init() { app.init(); fileInput.addEventListener('change', handleXmlFiles); } return { init, getData: app.getData }; })(); // --- MÓDULO MANUAL --- const manualApp = (function() { const app = createLabelApp({ resultsDivId: 'manualResults', resultsCardId: 'manualResultsCard', outputFormatSelectId: 'manualOutputFormatSelect', downloadButtonsDivId: 'manualDownloadButtons', onDataChange: generateManualBarcode, renderCardContent: (item) => { let contentHTML = ` <div class="label-actions"> <button class="action-btn btn-view" title="Visualizar"><svg fill="#007bff" viewBox="0 0 16 16"><path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/><path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/></svg></button> <button class="action-btn btn-delete" title="Excluir"><svg fill="#dc3545" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg></button> </div>`; if (item.generateBarcode) { if (item.title) contentHTML += `<h3>${item.title}</h3>`; contentHTML += `<canvas></canvas>`; } else { contentHTML += `<pre>${item.barcodeData}</pre>`; } return contentHTML; }, renderModalContent: (item) => { const content = document.createElement('div'); if (item.generateBarcode) { if(item.title) content.innerHTML = ``; const canvas = document.createElement('canvas'); content.appendChild(canvas); const bcid = barcodeTypeSelect.value; let text = normalizeMultiline(item.barcodeData); if (bcid === 'code128') text = text.split('\n')[0]; bwipjs.toCanvas(canvas, { bcid, text, eclevel: bcid === 'pdf417' ? 2 : 'L' }); } else { content.innerHTML = ``; } return content; } }); const manualInput = document.getElementById('manualInput'); const manualOutputType = document.getElementById('manualOutputType'); function generateManualBarcode() { const item = app.getData()[0]; if (!item || !item.generateBarcode) return; const canvas = document.querySelector(`#manualResults .label-card[data-id="${item.id}"] canvas`); if(canvas) { try { const bcid = barcodeTypeSelect.value; let text = normalizeMultiline(item.barcodeData); if (bcid === 'code128') text = text.split('\n')[0]; bwipjs.toCanvas(canvas, { bcid, text, eclevel: bcid === 'pdf417' ? 2 : 'L' }); } catch (e) { canvas.outerHTML = ``; } } } function handleManualText() { const text = normalizeMultiline(manualInput.value); if(!text) { app.setData([]); app.display(); return; }; const type = manualOutputType.value; const generateCode = type === 'text_and_code' || type === 'code_only'; const showText = type === 'text_and_code' || type === 'text_only'; const title = showText && generateCode ? text.split('\n')[0] : ''; app.setData([{ id: `manual-${Date.now()}`, title: title, barcodeData: text, generateBarcode: generateCode }]); app.display(); if (generateCode) generateManualBarcode(); } function init() { app.init(); document.getElementById('processManualBtn').addEventListener('click', handleManualText); } return { init, getData: app.getData, generateManualBarcode }; })(); // --- GERENCIAMENTO DE ABAS E EVENTOS GLOBAIS --- const tabLinks = document.querySelectorAll('.tab-link'); const tabContents = document.querySelectorAll('.tab-content'); const resultCards = document.querySelectorAll('.card[id$="ResultsCard"]'); tabLinks.forEach(link => { link.addEventListener('click', () => { const activeTab = link.getAttribute('data-tab'); tabLinks.forEach(item => item.classList.remove('active')); tabContents.forEach(item => item.classList.remove('active')); link.classList.add('active'); document.getElementById(activeTab + 'Tab').classList.add('active'); resultCards.forEach(card => card.style.display = 'none'); statusBox.style.display = 'none'; const appDataMap = { excel: excelApp.getData(), xml: xmlApp.getData(), manual: manualApp.getData() }; if (appDataMap[activeTab] && appDataMap[activeTab].length > 0) { document.getElementById(activeTab + 'ResultsCard').style.display = 'block'; } }); }); modalClose.onclick = () => { modalView.style.display = 'none'; }; modalView.onclick = (e) => { if (e.target === modalView) modalView.style.display = 'none'; }; settingsTriggerBtn.onclick = () => { settingsModal.style.display = 'flex'; }; settingsModalClose.onclick = () => { settingsModal.style.display = 'none'; }; settingsModal.onclick = (e) => { if (e.target === settingsModal) settingsModal.style.display = 'none'; }; barcodeTypeSelect.addEventListener('change', () => { if (excelApp.getData().length > 0) { excelApp.generateAllBarcodes(); } if(manualApp.getData().length > 0){ manualApp.generateManualBarcode(); } }); // --- INICIALIZAÇÃO DOS MÓDULOS --- excelApp.init(); xmlApp.init(); manualApp.init(); }); </script> </body> </html>
Pronto! Agora a sequência fica exatamente “um abaixo do outro”, sem espaço/linha extra no final, tanto no PDF/preview quanto no arquivo ZPL para as Zebras.