Consulta produtos
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
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ả
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 expirouDigite 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
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- 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.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của vercel/examples
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
Maintainer thường phản hồi trong vòng 1 ngày
-
[next-chat] Typing indicator gets stuck when a socket disconnects mid-typingCó thể đã có người làm @officialasishkumar đã nhận 12 ngày trước. Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 55/100
Maintainer thường phản hồi trong vòng 1 ngày
-
CARGO-TAXIĐang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 10/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Better ChoicezĐang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 15/100
vercel/examples#1584 · 2 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
onbeat landing pageĐang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 32/100
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của vercel/examples
Issue tương tự
-
[BUG]: Duplicate preposition in heading and unformatted feedback text on Strengthen Argument pageĐang mởbug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
AOSSIE-Org/DebateAI#801 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
Maintainer thường phản hồi trong vòng 1 ngày
-
ci: repo has no GitHub Actions workflow, so pushes and pull requests are never built or testedĐang mởagents area: runtime automation chore cli consistency dependencies developer-experience difficulty: easy documentation enhancement good first issue help wanted hygiene json-output priority: P3 starters tests
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
-
Add: BOM CineĐang mởapproved check:passed streams:add
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày