diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 0000000..bdfcecc --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1 @@ +{"projectId":"prj_d4Snf7Qo4GMchiUVyezwVqJieNi6","orgId":"team_hyS3Eg5TitOtyQ1YVgGLzKUz","projectName":"trae_table_ls74","neverMindDeployCard":true} \ No newline at end of file diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..a48e5d3 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,7 @@ +node_modules +build +dist +.git +.trae +.log +.figma \ No newline at end of file diff --git a/static/app.js b/static/app.js index e46e8e3..b8aef4c 100644 --- a/static/app.js +++ b/static/app.js @@ -14,6 +14,8 @@ const closeScanner = qs('#closeScanner'); let scannerInst = null; let videoEl = null; let mediaStream = null; +const scannerMessage = qs('#scannerMessage'); +const captureInput = qs('#captureInput'); // 无分页模式 let currentTab = 'search'; @@ -81,7 +83,15 @@ 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 formats = ['ean_13','ean_8','code_128','code_39','upc_a','upc_e']; const detector = new window.BarcodeDetector({ formats }); @@ -94,6 +104,7 @@ async function openScanner(){ host.innerHTML = ''; host.appendChild(videoEl); await videoEl.play(); + scannerMessage.classList.add('hidden'); const loop = async()=>{ try{ const codes = await detector.detect(videoEl); @@ -110,10 +121,12 @@ async function openScanner(){ }; requestAnimationFrame(loop); } else { - scannerModal.classList.add('hidden'); + scannerMessage.textContent = '浏览器不支持实时扫码,已切换为拍照识别方式'; + captureInput.click(); } }catch(e){ - scannerModal.classList.add('hidden'); + scannerMessage.textContent = '无法启用摄像头,已切换为拍照识别方式'; + captureInput.click(); } } @@ -121,9 +134,39 @@ 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(_){ } 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 = ''; + } +}); + scanBtn.addEventListener('click', openScanner); closeScanner.addEventListener('click', closeScannerFn); diff --git a/static/index.html b/static/index.html index 208e78a..3897922 100644 --- a/static/index.html +++ b/static/index.html @@ -18,7 +18,13 @@ @@ -36,10 +42,12 @@ + diff --git a/static/styles.css b/static/styles.css index eb27707..5a36125 100644 --- a/static/styles.css +++ b/static/styles.css @@ -11,7 +11,10 @@ h1{font-size:20px;margin:8px 0 16px} input{flex:1;padding:10px;border:1px solid #ccc;border-radius:6px;background:#fff;color:#111} .search-input{position:relative;flex:1} .search-input input{width:100%;padding-right:44px} -.scan-btn{position:absolute;right:8px;top:50%;transform:translateY(-50%);border:0;background:#f0f0f0;color:#111;border-radius:6px;padding:6px 10px} +.scan-btn{position:absolute;right:8px;top:50%;transform:translateY(-50%);border:1px solid #ddd;background:#fafafa;color:#111;border-radius:6px;padding:6px 10px;display:flex;align-items:center;justify-content:center;cursor:pointer} +.scan-btn:hover{background:#f0f0f0} +.scan-btn:active{background:#e6e6e6} +.scan-btn svg{display:block} button{padding:10px 14px;border:0;border-radius:6px;background:#1976d2;color:#fff} .results{display:flex;flex-direction:column;gap:12px} .card{border:1px solid #eee;border-radius:10px;padding:12px;box-shadow:0 1px 2px rgba(0,0,0,0.04)} @@ -27,6 +30,8 @@ mark{background:#ffec99} .loading{color:#1976d2} .status{margin-top:8px;color:#555} .import{margin-top:12px;display:flex;gap:8px;align-items:center} +.scanner-message{margin-bottom:8px;color:#555} +.scanner-message.hidden{display:none} .modal{position:fixed;inset:0;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center} .modal.hidden{display:none} .modal-content{background:#fff;border-radius:10px;padding:12px;width:92%;max-width:480px} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..c6c779f --- /dev/null +++ b/vercel.json @@ -0,0 +1 @@ +{"rewrites":[{"source":"/(.*)","destination":"/index.html"}]} \ No newline at end of file