const qs = (s) => document.querySelector(s); const results = qs('#results'); const input = qs('#q'); const searchBtn = qs('#searchBtn'); const fileInput = qs('#file'); const importBtn = qs('#importBtn'); const importStatus = qs('#importStatus'); const tabs = document.querySelectorAll('.tab'); const searchSection = qs('#searchSection'); const importSection = qs('#importSection'); const scanBtn = qs('#scanBtn'); const scannerModal = qs('#scannerModal'); const closeScanner = qs('#closeScanner'); let scannerInst = null; let videoEl = null; let mediaStream = null; const scannerMessage = qs('#scannerMessage'); const captureInput = qs('#captureInput'); const toggleTorchBtn = qs('#toggleTorch'); const zoomInput = qs('#zoom'); const zoomWrap = qs('#zoomWrap'); let detector = null; let videoTrack = null; let loopReq = null; // 无分页模式 let currentTab = 'search'; let limit = 10000; let currentQuery = ''; function setTab(tab){ currentTab = tab; tabs.forEach(t=>{ const active = t.dataset.tab===tab; t.classList.toggle('active', active); t.setAttribute('aria-selected', String(active)); }); if(tab==='search'){ searchSection.classList.remove('hidden'); importSection.classList.add('hidden'); } else { importSection.classList.remove('hidden'); searchSection.classList.add('hidden'); } } tabs.forEach(t=>t.addEventListener('click',()=>setTab(t.dataset.tab))); setTab('search'); async function fetchResults(){ results.innerHTML = '
加载中...
'; try{ const r = await fetch(`/products?q=${encodeURIComponent(currentQuery)}&limit=${limit}`); const d = await r.json(); render(d); }catch(e){ results.innerHTML = '
请求失败
'; } } function search(){ currentQuery = input.value.trim(); fetchResults(); } function render(items) { if (!items || items.length === 0) { results.innerHTML = '
无结果
'; return; } const term = currentQuery; const esc = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const re = term ? new RegExp(esc, 'ig') : null; results.innerHTML = items.map(x => { const name = x.name ?? ''; const purchase = x.purchase_price ?? ''; const sale = x.sale_price ?? ''; const barcode = x.barcode ?? ''; const nameH = re ? name.replace(re, m=>`${m}`) : name; const barcodeH = re ? barcode.replace(re, m=>`${m}`) : barcode; return `
${nameH}
卖价:${sale}
进价:${purchase}
条码:${barcodeH}
`; }).join(''); // 无分页模式 } searchBtn.addEventListener('click', search); input.addEventListener('keydown', e => { if (e.key === 'Enter') search(); }); async function openScanner(){ scannerModal.classList.remove('hidden'); scannerMessage.textContent = '正在请求摄像头权限...'; scannerMessage.classList.remove('hidden'); const isSecure = location.protocol === 'https:' || location.hostname === 'localhost'; try{ if(!isSecure){ scannerMessage.textContent = '非安全上下文,已切换为拍照识别方式'; captureInput.click(); return; } if('BarcodeDetector' in window){ const supported = await window.BarcodeDetector.getSupportedFormats(); const wanted = ['ean_13','ean_8','code_128','code_39','upc_a','upc_e']; const useFormats = wanted.filter(f=>supported.includes(f)); detector = new window.BarcodeDetector({ formats: useFormats.length?useFormats:supported }); mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); videoEl = document.createElement('video'); videoEl.autoplay = true; videoEl.playsInline = true; videoEl.srcObject = mediaStream; const host = document.getElementById('scanner'); host.innerHTML = ''; host.appendChild(videoEl); await videoEl.play(); scannerMessage.classList.add('hidden'); try{ videoTrack = mediaStream.getVideoTracks()[0]; const caps = videoTrack.getCapabilities ? videoTrack.getCapabilities() : {}; if(caps.torch){ toggleTorchBtn.classList.remove('hidden'); } else { toggleTorchBtn.classList.add('hidden'); } if(caps.zoom){ zoomWrap.classList.remove('hidden'); const min = caps.zoom.min ?? 1; const max = caps.zoom.max ?? 5; zoomInput.min = String(min); zoomInput.max = String(max); zoomInput.value = String(min); } else { zoomWrap.classList.add('hidden'); } }catch(_){ } const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const tick = async()=>{ try{ const w = videoEl.videoWidth || 640; const h = videoEl.videoHeight || 480; const size = Math.min(w, h); const sx = (w - size)/2; const sy = (h - size)/2; canvas.width = size; canvas.height = size; ctx.drawImage(videoEl, sx, sy, size, size, 0, 0, size, size); const codes = await detector.detect(canvas); if(codes && codes.length){ const text = codes[0].rawValue; input.value = text; currentQuery = text; fetchResults(); closeScannerFn(); return; } }catch(_){ } if(!scannerModal.classList.contains('hidden')){ loopReq = requestAnimationFrame(tick); } }; loopReq = requestAnimationFrame(tick); } else { scannerMessage.textContent = '浏览器不支持实时扫码,已切换为拍照识别方式'; captureInput.click(); } }catch(e){ scannerMessage.textContent = '无法启用摄像头,已切换为拍照识别方式'; captureInput.click(); } } function closeScannerFn(){ try{ if(scannerInst){ scannerInst.stop().then(()=>scannerInst.clear()); } }catch(_){ } try{ if(mediaStream){ mediaStream.getTracks().forEach(t=>t.stop()); mediaStream=null; } }catch(_){ } try{ const host = document.getElementById('scanner'); if(host){ host.innerHTML=''; } }catch(_){ } try{ if(scannerMessage){ scannerMessage.textContent=''; scannerMessage.classList.add('hidden'); } }catch(_){ } try{ if(loopReq){ cancelAnimationFrame(loopReq); loopReq=null; } }catch(_){ } scannerModal.classList.add('hidden'); } captureInput.addEventListener('change', async ()=>{ const file = captureInput.files?.[0]; if(!file){ return; } try{ if('BarcodeDetector' in window){ const img = await createImageBitmap(file); const detector = new window.BarcodeDetector({ formats: ['ean_13','ean_8','code_128','code_39','upc_a','upc_e'] }); const codes = await detector.detect(img); if(codes && codes.length){ const text = codes[0].rawValue; input.value = text; currentQuery = text; fetchResults(); } else { scannerMessage.textContent = '未识别到条码,请重试或手动输入'; scannerMessage.classList.remove('hidden'); } } else { scannerMessage.textContent = '当前浏览器不支持条码识别,请手动输入'; scannerMessage.classList.remove('hidden'); } }catch(_){ scannerMessage.textContent = '识别失败,请重试或手动输入'; scannerMessage.classList.remove('hidden'); } finally { captureInput.value = ''; } }); toggleTorchBtn.addEventListener('click', async ()=>{ try{ if(!videoTrack) return; const settings = videoTrack.getSettings ? videoTrack.getSettings() : {}; const torchOn = settings.torch === true; await videoTrack.applyConstraints({ advanced: [{ torch: !torchOn }] }); }catch(_){ } }); zoomInput.addEventListener('input', async ()=>{ try{ if(!videoTrack) return; const z = Number(zoomInput.value); await videoTrack.applyConstraints({ advanced: [{ zoom: z }] }); }catch(_){ } }); scanBtn.addEventListener('click', openScanner); closeScanner.addEventListener('click', closeScannerFn); // 无分页控件 importBtn.addEventListener('click', async () => { const f = fileInput.files?.[0]; if (!f) return; const fd = new FormData(); fd.append('file', f); importStatus.textContent = '导入中...'; try { const r = await fetch('/import', { method: 'POST', body: fd }); const d = await r.json(); importStatus.textContent = `插入: ${d.inserted ?? 0},更新: ${d.updated ?? 0},跳过: ${d.skipped ?? 0}`; } catch (e) { importStatus.textContent = '导入失败'; } });