Consulta produtos

Đang mở
#1,491 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức phù hợp với người mới
20/100
Loại issue
Tính năng
Độ rõ ràng
Cần làm rõ
Mức độ hoạt động
Ít trao đổi
Công nghệ
javascript
Lĩnh vực
frontend, web-dev

Hướng nghiên cứu

Trước tiên, hãy mở tài liệu HTML độc lập được cung cấp trong trình duyệt và xem xét entry point JavaScript inline của tài liệu đó. Issue không nêu tệp nào trong repository, bài kiểm thử hay thay đổi cụ thể; hãy làm rõ cách tích hợp chức năng tra cứu sản phẩm này với repository examples và xác định các tiêu chí nghiệm thu trước khi triển khai.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

<title>Consulta de Produtos - Código de Barras</title> <style> * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
    body {
        font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
        background: #f0f2f5;
        margin: 0;
        padding: 16px;
        color: #1e293b;
    }

    .container {
        max-width: 700px;
        margin: 0 auto;
    }

    .card {
        background: white;
        border-radius: 28px;
        box-shadow: 0 8px 20px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.02);
        padding: 20px;
        margin-bottom: 20px;
        transition: all 0.2s;
    }

    .header {
        text-align: center;
        margin-bottom: 20px;
    }

    .header h1 {
        font-size: 1.7rem;
        font-weight: 600;
        margin: 0 0 4px 0;
        background: linear-gradient(135deg, #2b6e3c, #1e8e3e);
        background-clip: text;
        -webkit-background-clip: text;
        color: transparent;
    }

    .header p {
        font-size: 0.85rem;
        color: #5b6e8c;
        margin: 0;
    }

    .import-area {
        background: #f8fafc;
        border: 1.5px dashed #94a3b8;
        border-radius: 20px;
        padding: 16px;
        text-align: center;
        margin-bottom: 24px;
        transition: background 0.2s;
    }

    .import-label {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        background: #1e8e3e;
        color: white;
        padding: 10px 20px;
        border-radius: 40px;
        font-weight: 500;
        font-size: 0.9rem;
        cursor: pointer;
        transition: background 0.2s;
        box-shadow: 0 1px 2px rgba(0,0,0,0.05);
    }

    .import-label:active {
        background: #166b2e;
        transform: scale(0.97);
    }

    .import-label input {
        display: none;
    }

    .file-status {
        font-size: 0.8rem;
        margin-top: 10px;
        color: #2c6b2f;
        font-weight: 500;
    }

    .stats {
        display: flex;
        justify-content: space-between;
        background: #eef2ff;
        padding: 12px 16px;
        border-radius: 60px;
        margin-bottom: 24px;
        font-size: 0.85rem;
        font-weight: 500;
    }

    .search-box {
        display: flex;
        gap: 12px;
        align-items: center;
        flex-wrap: wrap;
        margin-bottom: 24px;
    }

    .search-input-wrapper {
        flex: 1;
        position: relative;
    }

    .search-input-wrapper input {
        width: 100%;
        padding: 16px 18px;
        font-size: 1rem;
        border: 1.5px solid #e2e8f0;
        border-radius: 48px;
        background: white;
        transition: all 0.2s;
        font-family: monospace;
        letter-spacing: 0.3px;
    }

    .search-input-wrapper input:focus {
        outline: none;
        border-color: #2b6e3c;
        box-shadow: 0 0 0 3px rgba(30,142,62,0.2);
    }

    .scan-btn {
        background: #1e293b;
        color: white;
        border: none;
        border-radius: 48px;
        padding: 0 22px;
        font-size: 1rem;
        font-weight: 500;
        cursor: pointer;
        display: flex;
        align-items: center;
        gap: 6px;
        transition: 0.1s linear;
        height: 54px;
    }

    .scan-btn:active {
        background: #0f172a;
        transform: scale(0.96);
    }

    .result-card {
        background: white;
        border-radius: 28px;
        padding: 20px;
        box-shadow: 0 4px 12px rgba(0,0,0,0.05);
    }

    .product-found {
        border-left: 6px solid #1e8e3e;
    }

    .product-notfound {
        border-left: 6px solid #dc2626;
        text-align: center;
        color: #7f8c8d;
    }

    .product-title {
        font-size: 1.5rem;
        font-weight: 700;
        margin-bottom: 8px;
        word-break: break-word;
    }

    .barcode {
        font-family: monospace;
        background: #f1f5f9;
        padding: 6px 12px;
        border-radius: 40px;
        display: inline-block;
        font-size: 0.85rem;
        margin-bottom: 16px;
    }

    .info-row {
        display: flex;
        justify-content: space-between;
        padding: 10px 0;
        border-bottom: 1px solid #edf2f7;
    }

    .info-label {
        font-weight: 600;
        color: #4a5b7a;
    }

    .info-value {
        font-weight: 500;
        text-align: right;
        word-break: break-word;
        max-width: 60%;
    }

    .price {
        font-size: 1.4rem;
        font-weight: 800;
        color: #2b6e3c;
    }

    .help-text {
        font-size: 0.75rem;
        text-align: center;
        margin-top: 24px;
        color: #6c7a91;
    }

    /* Timer bar */
    .timer-container {
        margin-top: 16px;
        padding: 8px 12px;
        background: #f8fafc;
        border-radius: 60px;
        display: flex;
        align-items: center;
        gap: 12px;
        font-size: 0.8rem;
    }

    .timer-bar {
        flex: 1;
        height: 6px;
        background: #e2e8f0;
        border-radius: 10px;
        overflow: hidden;
    }

    .timer-progress {
        width: 0%;
        height: 100%;
        background: #1e8e3e;
        border-radius: 10px;
        transition: width 0.1s linear;
    }

    .timer-text {
        font-weight: 500;
        color: #475569;
        min-width: 65px;
        text-align: right;
    }

    button, .import-label {
        cursor: pointer;
        user-select: none;
    }

    @media (max-width: 480px) {
        body {
            padding: 12px;
        }
        .search-box {
            flex-direction: column;
            gap: 8px;
        }
        .scan-btn {
            width: 100%;
            justify-content: center;
        }
        .info-row {
            flex-direction: column;
            gap: 4px;
        }
        .info-value {
            text-align: left;
            max-width: 100%;
        }
        .timer-container {
            font-size: 0.7rem;
        }
    }
</style>

📦 Leitor de Estoque

consulta por código de barras (EAN-13 / GTIN)

    <div class="import-area" id="importArea">
        <label class="import-label">
            📂 IMPORTAR CADASTRO
            <input type="file" id="fileInput" accept=".html,.htm,.txt,.csv,text/html,text/plain">
        </label>
        <div id="fileStatus" class="file-status">Nenhum arquivo carregado. Clique para importar o relatório.</div>
        <div id="recordCount" style="font-size:12px; margin-top:6px; color:#475569;"></div>
    </div>

    <div class="stats" id="statsPanel">
        <span>📋 Produtos: <strong id="totalProducts">0</strong></span>
        <span>✅ Consultas: <strong id="queryCount">0</strong></span>
    </div>

    <div class="search-box">
        <div class="search-input-wrapper">
            <input type="text" id="barcodeInput" placeholder="🔍 Digite ou cole o código de barras" autocomplete="off" enterkeyhint="search">
        </div>
        <button class="scan-btn" id="searchBtn">
            🔎 Consultar
        </button>
    </div>

    <!-- Timer de limpeza automática -->
    <div class="timer-container" id="timerContainer" style="display: none;">
        <span>🕐 Limpeza automática:</span>
        <div class="timer-bar">
            <div class="timer-progress" id="timerProgress"></div>
        </div>
        <span class="timer-text" id="timerText">5.0s</span>
    </div>

    <div class="result-card" id="resultArea">
        <div style="text-align: center; color: #94a3b8; padding: 16px;">
            ⚡ Aguardando consulta<br>
            <span style="font-size: 13px;">Carregue o arquivo e pesquise por código EAN</span>
        </div>
    </div>
    <div class="help-text">
        💡 Dica: Use o arquivo "RELATORIO FISCAL + VALORES.html". <br>
        Após cada consulta, o resultado e o campo são limpos automaticamente em 5 segundos.
    </div>
</div>
<script> // -------------------------------------------------------------- // Estrutura: produtos em Map (chave = EAN-13) // -------------------------------------------------------------- let productMap = new Map(); let totalImported = 0; let queryCounter = 0; // Controle do timer let clearTimer = null; let timerInterval = null; let remainingTime = 0; let isTimerRunning = false; // Elementos DOM const fileInput = document.getElementById('fileInput'); const fileStatusDiv = document.getElementById('fileStatus'); const recordCountSpan = document.getElementById('recordCount'); const totalProductsSpan = document.getElementById('totalProducts'); const queryCountSpan = document.getElementById('queryCount'); const barcodeInput = document.getElementById('barcodeInput'); const searchBtn = document.getElementById('searchBtn'); const resultArea = document.getElementById('resultArea'); const timerContainer = document.getElementById('timerContainer'); const timerProgress = document.getElementById('timerProgress'); const timerText = document.getElementById('timerText'); // Atualiza contadores na tela function updateStatsUI() { totalProductsSpan.innerText = productMap.size; queryCountSpan.innerText = queryCounter; recordCountSpan.innerText = productMap.size > 0 ? `✅ ${productMap.size} produtos indexados` : '⚡ Nenhum produto carregado'; if (productMap.size === 0) { fileStatusDiv.innerText = 'Nenhum arquivo carregado. Clique para importar o relatório.'; fileStatusDiv.style.color = '#b91c1c'; } else { fileStatusDiv.innerText = `📁 Arquivo carregado com sucesso (${productMap.size} registros)`; fileStatusDiv.style.color = '#2c6b2f'; } } // Para o timer de limpeza function stopClearTimer() { if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } if (clearTimer) { clearTimeout(clearTimer); clearTimer = null; } isTimerRunning = false; timerContainer.style.display = 'none'; } // Inicia timer de 5 segundos para limpar o campo e resultado function startClearTimer() { // Para qualquer timer anterior stopClearTimer(); remainingTime = 5.0; timerContainer.style.display = 'flex'; timerProgress.style.width = '100%'; timerText.innerText = '5.0s'; isTimerRunning = true; // Atualiza barra a cada 100ms timerInterval = setInterval(() => { if (remainingTime > 0) { remainingTime -= 0.1; const percent = (remainingTime / 5.0) * 100; timerProgress.style.width = `${percent}%`; timerText.innerText = `${remainingTime.toFixed(1)}s`; } }, 100); // Timer final para limpar clearTimer = setTimeout(() => { // Limpa o campo de busca barcodeInput.value = ''; // Restaura o resultado para estado inicial if (productMap.size > 0) { resultArea.innerHTML = ` 🔄 Pesquisa expirou
Digite um novo código de barras `; } else { resultArea.innerHTML = ` ⚡ Aguardando consulta
Carregue o arquivo e pesquise por código EAN `; } // Foca no campo para nova consulta barcodeInput.focus(); stopClearTimer(); }, 5000); } // Extrai preço de custo (converte vírgula para ponto) function parsePrice(priceStr) { if (!priceStr) return 0; let cleaned = priceStr.toString().trim().replace(',', '.'); let num = parseFloat(cleaned); return isNaN(num) ? 0 : num; } function formatMoney(value) { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value); } function normalizeBarcode(raw) { if (!raw) return ''; return raw.toString().trim().replace(/\s+/g, '').replace(/-/g, ''); } function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>]/g, function(m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }); } function showResult(product, searchedCode) { if (!product) { resultArea.innerHTML = ` 🔍❌ Código não encontrado 🔢 ${escapeHtml(searchedCode)} Verifique o código ou recarregue o arquivo. `; return; } const custoFormatado = formatMoney(product.precoCusto); resultArea.innerHTML = ` ${escapeHtml(product.descricao)} 📌 Código: ${escapeHtml(product.ean)} 💰 PREÇO DE CUSTO ${custoFormatado} 🆔 ID do produto ${product.id} 📦 Unidade ${escapeHtml(product.medida || '—')} 🏷️ NCM ${escapeHtml(product.ncm || '—')} ⚖️ Situação Tributária ${escapeHtml(product.situacaoTributaria || '—')} `; } // PARSER DO ARQUIVO HTML function parseHtmlFileAndIndex(htmlString) { const parser = new DOMParser(); const doc = parser.parseFromString(htmlString, 'text/html'); const tabela = doc.querySelector('table.tabela'); if (!tabela) { throw new Error('Arquivo inválido: tabela com classe "tabela" não encontrada.'); } const rows = tabela.querySelectorAll('tr'); if (rows.length < 2) { throw new Error('Arquivo sem dados (sem linhas de produtos).'); } let columnMap = { id: -1, ean: -1, descricao: -1, precoCusto: -1, medida: -1, ncm: -1, situacaoTributaria: -1 }; for (let i = 0; i < rows.length; i++) { const ths = rows[i].querySelectorAll('th'); if (ths.length > 0) { for (let idx = 0; idx < ths.length; idx++) { const text = ths[idx].innerText.trim().toUpperCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); if (text === 'ID') columnMap.id = idx; else if (text === 'EAN-13') columnMap.ean = idx; else if (text === 'DESCRICAO') columnMap.descricao = idx; else if (text === 'PRECO CUSTO') columnMap.precoCusto = idx; else if (text === 'MEDIDA') columnMap.medida = idx; else if (text === 'NCM') columnMap.ncm = idx; else if (text === 'SITUACAO TRIBUTARIA') columnMap.situacaoTributaria = idx; } break; } } if (columnMap.id === -1 || columnMap.ean === -1 || columnMap.descricao === -1 || columnMap.precoCusto === -1) { throw new Error('Colunas obrigatórias (ID, EAN-13, DESCRICAO, PRECO CUSTO) não encontradas.'); } const newMap = new Map(); for (let i = 0; i < rows.length; i++) { const tds = rows[i].querySelectorAll('td'); if (tds.length === 0) continue; const idCell = tds[columnMap.id]?.innerText.trim(); if (!idCell || isNaN(parseInt(idCell))) continue; const eanRaw = tds[columnMap.ean]?.innerText.trim() || ''; let eanCode = normalizeBarcode(eanRaw); if (eanCode === '') continue; const descricao = tds[columnMap.descricao]?.innerText.trim() || 'SEM DESCRIÇÃO'; const precoRaw = tds[columnMap.precoCusto]?.innerText.trim().replace('R$', '').replace(',', '.').trim(); let precoCusto = parsePrice(precoRaw); const medida = tds[columnMap.medida]?.innerText.trim() || ''; const ncm = tds[columnMap.ncm]?.innerText.trim() || ''; const situacaoTributaria = tds[columnMap.situacaoTributaria]?.innerText.trim() || ''; newMap.set(eanCode, { id: idCell, ean: eanCode, descricao: descricao, precoCusto: precoCusto, medida: medida, ncm: ncm, situacaoTributaria: situacaoTributaria }); } if (newMap.size === 0) { throw new Error('Nenhum produto válido com código de barras encontrado.'); } return newMap; } function loadFile(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(e) { try { const map = parseHtmlFileAndIndex(e.target.result); resolve(map); } catch (err) { reject(err); } }; reader.onerror = () => reject(new Error('Erro ao ler o arquivo.')); reader.readAsText(file, 'UTF-8'); }); } fileInput.addEventListener('change', async (event) => { const file = event.target.files[0]; if (!file) return; fileStatusDiv.innerText = '⏳ Processando arquivo... Aguarde.'; fileStatusDiv.style.color = '#a16207'; try { const newMap = await loadFile(file); productMap = newMap; updateStatsUI(); // Para qualquer timer pendente stopClearTimer(); resultArea.innerHTML = ` ✅ Arquivo carregado! ${productMap.size} produtos prontos.
🔎 Digite um código de barras para consultar. `; barcodeInput.value = ''; barcodeInput.focus(); } catch (error) { fileStatusDiv.innerText = `❌ Erro: ${error.message}`; fileStatusDiv.style.color = '#b91c1c'; resultArea.innerHTML = ` Falha ao processar arquivo.
${escapeHtml(error.message)} `; productMap.clear(); updateStatsUI(); } finally { fileInput.value = ''; } }); function performSearch() { let rawCode = barcodeInput.value.trim(); if (rawCode === "") { resultArea.innerHTML = `⚠️ Digite ou cole um código de barras.`; stopClearTimer(); return; } // Para timer anterior e inicia novo stopClearTimer(); queryCounter++; updateStatsUI(); const normalized = normalizeBarcode(rawCode); if (normalized === "") { showResult(null, rawCode); startClearTimer(); return; } const found = productMap.get(normalized); if (found) { showResult(found, normalized); } else { let altKey = normalized.replace(/^0+/, ''); let altFound = altKey !== normalized ? productMap.get(altKey) : null; if (altFound) { showResult(altFound, normalized); } else { showResult(null, rawCode); } } // Inicia timer para limpar após 5 segundos startClearTimer(); } searchBtn.addEventListener('click', performSearch); barcodeInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); performSearch(); } }); // Scanner com câmera const cameraBtn = document.createElement('button'); cameraBtn.innerText = '📷 Câmera'; cameraBtn.className = 'scan-btn'; cameraBtn.style.background = '#3b5249'; cameraBtn.style.padding = '0 16px'; cameraBtn.style.fontSize = '0.9rem'; cameraBtn.id = 'customCameraBtn'; const searchWrapper = document.querySelector('.search-box'); if (searchWrapper && !document.getElementById('customCameraBtn')) { searchWrapper.appendChild(cameraBtn); cameraBtn.addEventListener('click', async () => { if ('BarcodeDetector' in window) { try { const detector = new BarcodeDetector({ formats: ['ean_13', 'ean_8', 'code_128', 'code_39', 'upc_a', 'upc_e'] }); const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); const video = document.createElement('video'); video.srcObject = stream; video.setAttribute('playsinline', true); video.style.position = 'fixed'; video.style.top = '0'; video.style.left = '0'; video.style.width = '100%'; video.style.height = '100%'; video.style.objectFit = 'cover'; video.style.zIndex = '9999'; video.style.background = 'black'; document.body.appendChild(video); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); video.play(); const closeBtn = document.createElement('button'); closeBtn.innerText = '✖ Fechar Scanner'; closeBtn.style.position = 'fixed'; closeBtn.style.bottom = '30px'; closeBtn.style.left = '50%'; closeBtn.style.transform = 'translateX(-50%)'; closeBtn.style.zIndex = '10000'; closeBtn.style.padding = '12px 24px'; closeBtn.style.backgroundColor = '#dc2626'; closeBtn.style.color = 'white'; closeBtn.style.border = 'none'; closeBtn.style.borderRadius = '60px'; closeBtn.style.fontWeight = 'bold'; document.body.appendChild(closeBtn); let scanning = true; const scanInterval = setInterval(async () => { if (!scanning) return; if (video.videoWidth === 0) return; canvas.width = video.videoWidth; canvas.height = video.videoHeight; context.drawImage(video, 0, 0, canvas.width, canvas.height); const imageData = context.getImageData(0, 0, canvas.width, canvas.height); try { const barcodes = await detector.detect(imageData); if (barcodes.length > 0) { const code = barcodes[0].rawValue; scanning = false; clearInterval(scanInterval); stream.getTracks().forEach(track => track.stop()); video.remove(); closeBtn.remove(); barcodeInput.value = code; performSearch(); } } catch (err) {} }, 300); closeBtn.addEventListener('click', () => { scanning = false; clearInterval(scanInterval); stream.getTracks().forEach(track => track.stop()); video.remove(); closeBtn.remove(); }); } catch (err) { alert('Erro ao acessar câmera: ' + err.message); } } else { alert('Seu navegador não suporta leitura por câmera. Digite o código manualmente.'); } }); } updateStatsUI(); if (productMap.size === 0) { resultArea.innerHTML = ` 📂
Clique em "IMPORTAR CADASTRO" e selecione o arquivo
RELATORIO FISCAL + VALORES.html
Após carregar, consulte produtos por código de barras. `; } </script>
Ngôn ngữ chính
TypeScript
Star
5.2k
Fork
1.9k
Merge trung bình
1 giờ 4 phút
Pull request đã merge (30 ngày)
2

Chuẩn bị môi trường

  • Không có Dockerfile hay tệp Docker Compose
  • Có mẫu pull request
  • Không có hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của vercel/examples

Tất cả issue của vercel/examples

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.