1// ============================================ 2// MODULE: Quét QR Code bằng camera 3// ============================================ 4 5let html5QrCode = null; 6let isScanning = false; 7let canScan = true; 8let scanCooldown = 1500; 9 10function playSuccessBeep() { 11 try { 12 const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 13 const oscillator = audioCtx.createOscillator(); 14 const gainNode = audioCtx.createGain(); 15 16 oscillator.connect(gainNode); 17 gainNode.connect(audioCtx.destination); 18 19 oscillator.frequency.value = 880; 20 gainNode.gain.value = 0.3; 21 22 oscillator.start(); 23 gainNode.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.3); 24 oscillator.stop(audioCtx.currentTime + 0.3); 25 26 if (audioCtx.state === 'suspended') { 27 audioCtx.resume(); 28 } 29 } catch(e) { 30 console.log('Không thá» phát âm thanh:', e); 31 } 32} 33 34function playDuplicateBeep() { 35 try { 36 const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 37 const oscillator = audioCtx.createOscillator(); 38 const gainNode = audioCtx.createGain(); 39 40 oscillator.connect(gainNode); 41 gainNode.connect(audioCtx.destination); 42 43 oscillator.frequency.value = 440; 44 gainNode.gain.value = 0.3; 45 46 oscillator.start(); 47 gainNode.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.2); 48 oscillator.stop(audioCtx.currentTime + 0.2); 49 50 if (audioCtx.state === 'suspended') { 51 audioCtx.resume(); 52 } 53 } catch(e) { 54 console.log('Không thá» phát âm thanh:', e); 55 } 56} 57 58function showToast(message, type = 'success') { 59 let toast = document.getElementById('qr-toast'); 60 if (!toast) { 61 toast = document.createElement('div'); 62 toast.id = 'qr-toast'; 63 toast.style.cssText = ` 64 position: fixed; 65 bottom: 80px; 66 left: 50%; 67 transform: translateX(-50%); 68 background: rgba(0,0,0,0.85); 69 color: white; 70 padding: 10px 20px; 71 border-radius: 8px; 72 z-index: 10000; 73 font-size: 14px; 74 white-space: nowrap; 75 transition: opacity 0.3s; 76 pointer-events: none; 77 font-family: 'Segoe UI', Arial, sans-serif; 78 `; 79 document.body.appendChild(toast); 80 } 81 82 if (type === 'success') { 83 toast.style.background = 'rgba(40, 167, 69, 0.95)'; 84 } else if (type === 'error') { 85 toast.style.background = 'rgba(220, 53, 69, 0.95)'; 86 } else { 87 toast.style.background = 'rgba(255, 152, 0, 0.95)'; 88 } 89 90 toast.textContent = message; 91 toast.style.opacity = '1'; 92 toast.style.whiteSpace = 'normal'; 93 toast.style.maxWidth = '90%'; 94 toast.style.textAlign = 'center'; 95 96 setTimeout(() => { 97 toast.style.opacity = '0'; 98 }, 1500); 99} 100 101async function startQRScan() { 102 const qrReaderDiv = document.getElementById('qr-reader'); 103 const startBtn = document.getElementById('startScanBtn'); 104 const stopBtn = document.getElementById('stopScanBtn'); 105 106 if (!qrReaderDiv) return; 107 108 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { 109 showToast('â ï¸ Trình duyá»t không há» trợ camera', 'error'); 110 return; 111 } 112 113 try { 114 canScan = true; 115 qrReaderDiv.style.display = 'block'; 116 117 if (typeof Html5Qrcode === 'undefined') { 118 await loadHtml5QrcodeLibrary(); 119 } 120 121 html5QrCode = new Html5Qrcode("qr-reader"); 122 123 await html5QrCode.start( 124 { facingMode: "environment" }, 125 { fps: 15, qrbox: { width: 250, height: 250 }, aspectRatio: 1.0 }, 126 onQRCodeSuccess, 127 onQRCodeError 128 ); 129 130 isScanning = true; 131 startBtn.style.display = 'none'; 132 stopBtn.style.display = 'inline-block'; 133 showToast('ð· Camera Äã sẵn sà ng', 'success'); 134 135 } catch (error) { 136 console.error(error); 137 showToast('â Không thá» khá»i tạo camera', 'error'); 138 qrReaderDiv.style.display = 'none'; 139 } 140} 141 142async function stopQRScan() { 143 const qrReaderDiv = document.getElementById('qr-reader'); 144 const startBtn = document.getElementById('startScanBtn'); 145 const stopBtn = document.getElementById('stopScanBtn'); 146 147 if (html5QrCode && isScanning) { 148 try { 149 await html5QrCode.stop(); 150 html5QrCode.clear(); 151 } catch (error) { 152 console.error(error); 153 } 154 } 155 156 isScanning = false;
157 canScan = true; 158 html5QrCode = null; 159 qrReaderDiv.style.display = 'none'; 160 startBtn.style.display = 'inline-block'; 161 stopBtn.style.display = 'none'; 162} 163 164function onQRCodeSuccess(decodedText) { 165 if (!canScan) return; 166 canScan = false; 167 168 try { 169 const parsedData = parseQRCode(decodedText); 170 const newIndex = parsedData.index; 171 const currentData = getCurrentData(); 172 const isDuplicate = currentData.some(row => row.index === newIndex); 173 174 if (isDuplicate) { 175 playDuplicateBeep(); 176 showToast(`â ï¸ Index ${newIndex} Äã tá»n tại`, 'warning'); 177 setTimeout(() => { canScan = true; }, scanCooldown); 178 return; 179 } 180 181 // Nếu có Excel active, cáºp nháºt thá»i gian + lotNo (không lấy J,K,L,M từ Excel) 182 const lotNo = document.getElementById('lotNo')?.value || ''; 183 if (typeof excelState !== 'undefined' && excelState && excelState.isActive) { 184 const indexExists = excelState.indexMap.has(newIndex); 185 if (indexExists) { 186 // Chá» cáºp nháºt thá»i gian và lotNo, không ghi Äè J,K,L,M 187 updateExcelRow(newIndex, parsedData, lotNo, false); 188 } else { 189 // Thêm dòng má»i và o Excel 190 addExcelRow(newIndex, parsedData, lotNo); 191 } 192 } 193 194 addRow(parsedData); 195 playSuccessBeep(); 196 showToast(`â Äã thêm: ${newIndex}`, 'success'); 197 198 setTimeout(() => { canScan = true; }, scanCooldown); 199 200 } catch (error) { 201 playDuplicateBeep(); 202 showToast('â ' + error.message, 'error'); 203 setTimeout(() => { canScan = true; }, scanCooldown); 204 } 205} 206 207function onQRCodeError(error) { 208 // Bá» qua lá»i nhá» 209} 210 211function loadHtml5QrcodeLibrary() { 212 return new Promise((resolve, reject) => { 213 if (typeof Html5Qrcode !== 'undefined') { 214 resolve(); 215 return; 216 } 217 const script = document.createElement('script'); 218 script.src = 'https://unpkg.com/[email protected]/html5-qrcode.min.js'; 219 script.onload = () => resolve(); 220 script.onerror = () => reject(new Error('Không thá» tải thư viá»n QR')); 221 document.head.appendChild(script); 222 }); 223}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.